This commit is contained in:
@@ -71,5 +71,6 @@ dependencies {
|
|||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'org.json:json:20231013'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ import haus.nightmare.a3270.ui.SettingsDialog
|
|||||||
import haus.nightmare.a3270.ui.TerminalInputView
|
import haus.nightmare.a3270.ui.TerminalInputView
|
||||||
import haus.nightmare.a3270.ui.TerminalView
|
import haus.nightmare.a3270.ui.TerminalView
|
||||||
import haus.nightmare.a3270.ui.TwoRowKeyBar
|
import haus.nightmare.a3270.ui.TwoRowKeyBar
|
||||||
|
import haus.nightmare.a3270.ui.FieldInspectorDialog
|
||||||
|
import haus.nightmare.a3270.ui.FindDialog
|
||||||
|
import haus.nightmare.a3270.ui.ScreenExporterDialog
|
||||||
|
import haus.nightmare.a3270.ui.ScriptDialog
|
||||||
|
import haus.nightmare.a3270.ui.PrinterSessionDialog
|
||||||
|
|
||||||
import haus.nightmare.a3270.ui.UntrustedCertificateDialog
|
import haus.nightmare.a3270.ui.UntrustedCertificateDialog
|
||||||
|
|
||||||
@@ -401,6 +406,9 @@ fun MainScreen(
|
|||||||
val hapticFeedback by viewModel.hapticFeedback.collectAsState()
|
val hapticFeedback by viewModel.hapticFeedback.collectAsState()
|
||||||
val verifyCerts by viewModel.verifyCerts.collectAsState()
|
val verifyCerts by viewModel.verifyCerts.collectAsState()
|
||||||
val defaultGraphicsMode by viewModel.defaultGraphicsMode.collectAsState()
|
val defaultGraphicsMode by viewModel.defaultGraphicsMode.collectAsState()
|
||||||
|
val codePage by viewModel.codePage.collectAsState()
|
||||||
|
val searchHighlightAddr by viewModel.searchHighlightAddr.collectAsState()
|
||||||
|
val searchHighlightLen by viewModel.searchHighlightLen.collectAsState()
|
||||||
val isTlsActive by viewModel.isTlsActive.collectAsState()
|
val isTlsActive by viewModel.isTlsActive.collectAsState()
|
||||||
val isTlsVerified by viewModel.isTlsVerified.collectAsState()
|
val isTlsVerified by viewModel.isTlsVerified.collectAsState()
|
||||||
val untrustedCertPrompt by viewModel.untrustedCertPrompt.collectAsState()
|
val untrustedCertPrompt by viewModel.untrustedCertPrompt.collectAsState()
|
||||||
@@ -410,6 +418,11 @@ fun MainScreen(
|
|||||||
var showConnectDialog by remember { mutableStateOf(false) }
|
var showConnectDialog by remember { mutableStateOf(false) }
|
||||||
var showFtDialog by remember { mutableStateOf(false) }
|
var showFtDialog by remember { mutableStateOf(false) }
|
||||||
var showSettingsDialog by remember { mutableStateOf(false) }
|
var showSettingsDialog by remember { mutableStateOf(false) }
|
||||||
|
var showFieldInspectorDialog by remember { mutableStateOf(false) }
|
||||||
|
var showFindDialog by remember { mutableStateOf(false) }
|
||||||
|
var showScreenExportDialog by remember { mutableStateOf(false) }
|
||||||
|
var showScriptDialog by remember { mutableStateOf(false) }
|
||||||
|
var showPrinterSessionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
||||||
|
|
||||||
@@ -440,7 +453,8 @@ fun MainScreen(
|
|||||||
autoHost.useTls,
|
autoHost.useTls,
|
||||||
autoHost.tlsVerifyCert,
|
autoHost.tlsVerifyCert,
|
||||||
autoHost.tn3270e,
|
autoHost.tn3270e,
|
||||||
autoHost.graphicsMode
|
autoHost.graphicsMode,
|
||||||
|
autoHost.codePage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,6 +491,8 @@ fun MainScreen(
|
|||||||
isLightPenMode = isLightPenMode,
|
isLightPenMode = isLightPenMode,
|
||||||
maskHiddenFields = maskHiddenInput,
|
maskHiddenFields = maskHiddenInput,
|
||||||
blinkCursor = cursorBlink,
|
blinkCursor = cursorBlink,
|
||||||
|
searchHighlightAddr = searchHighlightAddr,
|
||||||
|
searchHighlightLen = searchHighlightLen,
|
||||||
onTapAddress = { addr ->
|
onTapAddress = { addr ->
|
||||||
viewModel.setCursor(addr)
|
viewModel.setCursor(addr)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
@@ -534,6 +550,11 @@ fun MainScreen(
|
|||||||
onConnectClick = { showConnectDialog = true },
|
onConnectClick = { showConnectDialog = true },
|
||||||
onDisconnectClick = { viewModel.disconnect() },
|
onDisconnectClick = { viewModel.disconnect() },
|
||||||
onFtClick = { showFtDialog = true },
|
onFtClick = { showFtDialog = true },
|
||||||
|
onFieldInspectorClick = { showFieldInspectorDialog = true },
|
||||||
|
onFindClick = { showFindDialog = true },
|
||||||
|
onScreenExportClick = { showScreenExportDialog = true },
|
||||||
|
onScriptClick = { showScriptDialog = true },
|
||||||
|
onPrinterSessionClick = { showPrinterSessionDialog = true },
|
||||||
onToggleLightPen = { viewModel.toggleLightPen() },
|
onToggleLightPen = { viewModel.toggleLightPen() },
|
||||||
onSettingsClick = { showSettingsDialog = true },
|
onSettingsClick = { showSettingsDialog = true },
|
||||||
onSendAid = { aid ->
|
onSendAid = { aid ->
|
||||||
@@ -552,6 +573,22 @@ fun MainScreen(
|
|||||||
viewModel.backTab()
|
viewModel.backTab()
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
},
|
},
|
||||||
|
onEraseInput = {
|
||||||
|
viewModel.eraseInput()
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
|
onAttn = {
|
||||||
|
viewModel.sendAid(AID_PA1)
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
|
onSysReq = {
|
||||||
|
viewModel.sendAid(AID_SYSREQ)
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
|
onCursorSelect = {
|
||||||
|
viewModel.sendAid(AID_SELECT)
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onCursorLeft = {
|
onCursorLeft = {
|
||||||
viewModel.cursorLeft()
|
viewModel.cursorLeft()
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
@@ -582,6 +619,7 @@ fun MainScreen(
|
|||||||
isTls = isTlsActive,
|
isTls = isTlsActive,
|
||||||
isTlsVerified = isTlsVerified,
|
isTlsVerified = isTlsVerified,
|
||||||
graphicsMode = defaultGraphicsMode,
|
graphicsMode = defaultGraphicsMode,
|
||||||
|
codePage = codePage,
|
||||||
isLightPenMode = isLightPenMode
|
isLightPenMode = isLightPenMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -590,9 +628,9 @@ fun MainScreen(
|
|||||||
if (showConnectDialog) {
|
if (showConnectDialog) {
|
||||||
ConnectDialog(
|
ConnectDialog(
|
||||||
onDismiss = { showConnectDialog = false },
|
onDismiss = { showConnectDialog = false },
|
||||||
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode ->
|
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp ->
|
||||||
showConnectDialog = false
|
showConnectDialog = false
|
||||||
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode)
|
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -602,6 +640,7 @@ fun MainScreen(
|
|||||||
FileTransferDialog(
|
FileTransferDialog(
|
||||||
initialHostType = viewModel.activeHostType,
|
initialHostType = viewModel.activeHostType,
|
||||||
ftProgressState = ftState,
|
ftProgressState = ftState,
|
||||||
|
client = viewModel.getClient(),
|
||||||
onDismiss = { showFtDialog = false },
|
onDismiss = { showFtDialog = false },
|
||||||
onStartTransfer = { config ->
|
onStartTransfer = { config ->
|
||||||
viewModel.startFileTransfer(config) { error ->
|
viewModel.startFileTransfer(config) { error ->
|
||||||
@@ -623,15 +662,71 @@ fun MainScreen(
|
|||||||
initialHapticFeedback = hapticFeedback,
|
initialHapticFeedback = hapticFeedback,
|
||||||
initialVerifyCerts = verifyCerts,
|
initialVerifyCerts = verifyCerts,
|
||||||
initialDefaultGraphicsMode = defaultGraphicsMode,
|
initialDefaultGraphicsMode = defaultGraphicsMode,
|
||||||
|
initialDefaultCodePage = codePage,
|
||||||
onDismiss = { showSettingsDialog = false },
|
onDismiss = { showSettingsDialog = false },
|
||||||
onSave = { mask, blink, haptic, verify, gfx ->
|
onSave = { mask, blink, haptic, verify, gfx, defaultCp ->
|
||||||
showSettingsDialog = false
|
showSettingsDialog = false
|
||||||
viewModel.updateSettings(mask, blink, haptic, verify, gfx)
|
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showFieldInspectorDialog) {
|
||||||
|
FieldInspectorDialog(
|
||||||
|
client = viewModel.getClient(),
|
||||||
|
screenVersion = screenVersion,
|
||||||
|
onDismiss = { showFieldInspectorDialog = false },
|
||||||
|
onJumpToField = { addr ->
|
||||||
|
viewModel.setCursor(addr)
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showFindDialog) {
|
||||||
|
FindDialog(
|
||||||
|
onDismiss = { showFindDialog = false },
|
||||||
|
onFind = { query: String, matchCase: Boolean, forward: Boolean ->
|
||||||
|
viewModel.find(query, matchCase, forward)
|
||||||
|
},
|
||||||
|
onClearHighlight = {
|
||||||
|
viewModel.clearSearchHighlight()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showScreenExportDialog) {
|
||||||
|
ScreenExporterDialog(
|
||||||
|
screenBuffer = screenBuffer,
|
||||||
|
rows = rows,
|
||||||
|
cols = cols,
|
||||||
|
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
||||||
|
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
||||||
|
maskHidden = maskHiddenInput,
|
||||||
|
onDismiss = { showScreenExportDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showScriptDialog) {
|
||||||
|
ScriptDialog(
|
||||||
|
onDismiss = { showScriptDialog = false },
|
||||||
|
onExecuteScript = { script: String, onStatus: (String) -> Unit, onComplete: (String?) -> Unit ->
|
||||||
|
viewModel.executeScript(script, onStatus, onComplete)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPrinterSessionDialog) {
|
||||||
|
val activeHost = viewModel.currentHost.ifBlank { "127.0.0.1" }
|
||||||
|
PrinterSessionDialog(
|
||||||
|
initialHost = activeHost,
|
||||||
|
initialCodePage = codePage,
|
||||||
|
initialUseTls = isTlsActive,
|
||||||
|
onDismiss = { showPrinterSessionDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val prompt = untrustedCertPrompt
|
val prompt = untrustedCertPrompt
|
||||||
if (prompt != null) {
|
if (prompt != null) {
|
||||||
UntrustedCertificateDialog(
|
UntrustedCertificateDialog(
|
||||||
|
|||||||
@@ -116,6 +116,15 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _defaultGraphicsMode = MutableStateFlow(AppSettings.getDefaultGraphicsMode(application))
|
private val _defaultGraphicsMode = MutableStateFlow(AppSettings.getDefaultGraphicsMode(application))
|
||||||
val defaultGraphicsMode: StateFlow<String> = _defaultGraphicsMode.asStateFlow()
|
val defaultGraphicsMode: StateFlow<String> = _defaultGraphicsMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _codePage = MutableStateFlow(AppSettings.getDefaultCodePage(application))
|
||||||
|
val codePage: StateFlow<String> = _codePage.asStateFlow()
|
||||||
|
|
||||||
|
private val _searchHighlightAddr = MutableStateFlow(-1)
|
||||||
|
val searchHighlightAddr: StateFlow<Int> = _searchHighlightAddr.asStateFlow()
|
||||||
|
|
||||||
|
private val _searchHighlightLen = MutableStateFlow(0)
|
||||||
|
val searchHighlightLen: StateFlow<Int> = _searchHighlightLen.asStateFlow()
|
||||||
|
|
||||||
private val _isTlsActive = MutableStateFlow(false)
|
private val _isTlsActive = MutableStateFlow(false)
|
||||||
val isTlsActive: StateFlow<Boolean> = _isTlsActive.asStateFlow()
|
val isTlsActive: StateFlow<Boolean> = _isTlsActive.asStateFlow()
|
||||||
|
|
||||||
@@ -136,6 +145,26 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_isLightPenMode.value = enabled
|
_isLightPenMode.value = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSearchHighlight(addr: Int, len: Int) {
|
||||||
|
_searchHighlightAddr.value = addr
|
||||||
|
_searchHighlightLen.value = len
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSearchHighlight() {
|
||||||
|
_searchHighlightAddr.value = -1
|
||||||
|
_searchHighlightLen.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCodePage(cp: String) {
|
||||||
|
_codePage.value = cp
|
||||||
|
val c = client
|
||||||
|
if (c != null) {
|
||||||
|
c.codePage = cp
|
||||||
|
c.screenBuffer?.translateToUnicode()
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File Transfer State
|
// File Transfer State
|
||||||
private var fileTransferCoordinator: FileTransfer? = null
|
private var fileTransferCoordinator: FileTransfer? = null
|
||||||
private val _ftState = MutableStateFlow(FTProgressState())
|
private val _ftState = MutableStateFlow(FTProgressState())
|
||||||
@@ -269,17 +298,92 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean, verifyCertsVal: Boolean = true, defaultGraphicsVal: String = "BOTH") {
|
fun updateSettings(
|
||||||
|
maskHidden: Boolean,
|
||||||
|
blink: Boolean,
|
||||||
|
haptic: Boolean,
|
||||||
|
verifyCertsVal: Boolean = true,
|
||||||
|
defaultGraphicsVal: String = "BOTH",
|
||||||
|
defaultCodePageVal: String = "037"
|
||||||
|
) {
|
||||||
AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden)
|
AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden)
|
||||||
AppSettings.setCursorBlinkEnabled(getApplication(), blink)
|
AppSettings.setCursorBlinkEnabled(getApplication(), blink)
|
||||||
AppSettings.setHapticFeedbackEnabled(getApplication(), haptic)
|
AppSettings.setHapticFeedbackEnabled(getApplication(), haptic)
|
||||||
AppSettings.setVerifyCertsEnabled(getApplication(), verifyCertsVal)
|
AppSettings.setVerifyCertsEnabled(getApplication(), verifyCertsVal)
|
||||||
AppSettings.setDefaultGraphicsMode(getApplication(), defaultGraphicsVal)
|
AppSettings.setDefaultGraphicsMode(getApplication(), defaultGraphicsVal)
|
||||||
|
AppSettings.setDefaultCodePage(getApplication(), defaultCodePageVal)
|
||||||
_maskHiddenInput.value = maskHidden
|
_maskHiddenInput.value = maskHidden
|
||||||
_cursorBlink.value = blink
|
_cursorBlink.value = blink
|
||||||
_hapticFeedback.value = haptic
|
_hapticFeedback.value = haptic
|
||||||
_verifyCerts.value = verifyCertsVal
|
_verifyCerts.value = verifyCertsVal
|
||||||
_defaultGraphicsMode.value = defaultGraphicsVal
|
_defaultGraphicsMode.value = defaultGraphicsVal
|
||||||
|
_codePage.value = defaultCodePageVal
|
||||||
|
}
|
||||||
|
|
||||||
|
fun find(target: String, matchCase: Boolean = false, forward: Boolean = true): Int {
|
||||||
|
val c = client ?: return -1
|
||||||
|
val sb = c.screenBuffer ?: return -1
|
||||||
|
val curAddr = sb.cursorAddress
|
||||||
|
val cols = sb.cols
|
||||||
|
val rows = sb.rows
|
||||||
|
if (cols <= 0 || rows <= 0) return -1
|
||||||
|
val curRow = curAddr / cols
|
||||||
|
val curCol = curAddr % cols
|
||||||
|
|
||||||
|
var startRow = curRow
|
||||||
|
var startCol = curCol
|
||||||
|
if (forward) {
|
||||||
|
startCol = (curCol + 1) % cols
|
||||||
|
if (startCol == 0) startRow = (curRow + 1) % rows
|
||||||
|
} else {
|
||||||
|
startCol = (curCol - 1 + cols) % cols
|
||||||
|
if (startCol == cols - 1) startRow = (curRow - 1 + rows) % rows
|
||||||
|
}
|
||||||
|
|
||||||
|
val dir = if (forward) haus.nightmare.lib3270j.ecl.ECLConstants.SEARCH_FORWARD else haus.nightmare.lib3270j.ecl.ECLConstants.SEARCH_BACKWARD
|
||||||
|
val foundPos = c.ps.searchString(target, startRow, startCol, dir, !matchCase)
|
||||||
|
if (foundPos >= 0) {
|
||||||
|
sb.cursorAddress = foundPos
|
||||||
|
_cursorAddress.value = foundPos
|
||||||
|
setSearchHighlight(foundPos, target.length)
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
return foundPos
|
||||||
|
}
|
||||||
|
|
||||||
|
fun executeScript(
|
||||||
|
script: String,
|
||||||
|
onStatus: (String) -> Unit = {},
|
||||||
|
onComplete: (String?) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val c = client
|
||||||
|
if (c == null || !_connectionState.value.isConnected()) {
|
||||||
|
onComplete("Not connected to host.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val cleaned = script.replace("\r\n", "\n").replace("\r", "\n")
|
||||||
|
val lines = cleaned.split("\n")
|
||||||
|
for (line in lines) {
|
||||||
|
val trimmed = line.trim()
|
||||||
|
if (trimmed.isNotEmpty() && !trimmed.startsWith("#") && !trimmed.startsWith("//")) {
|
||||||
|
withContext(Dispatchers.Main) { onStatus("Sending: $trimmed") }
|
||||||
|
c.sendKeys(trimmed)
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onComplete(null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warning("Script execution error: ${e.message}")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onComplete(e.message ?: "Script execution error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connect(
|
fun connect(
|
||||||
@@ -291,7 +395,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
useTls: Boolean = false,
|
useTls: Boolean = false,
|
||||||
tlsVerifyCert: Boolean = true,
|
tlsVerifyCert: Boolean = true,
|
||||||
tn3270e: Boolean = true,
|
tn3270e: Boolean = true,
|
||||||
graphicsModeStr: String = "BOTH"
|
graphicsModeStr: String = "BOTH",
|
||||||
|
codePageStr: String = "037"
|
||||||
) {
|
) {
|
||||||
currentHost = host
|
currentHost = host
|
||||||
activeHostType = hostType
|
activeHostType = hostType
|
||||||
@@ -299,6 +404,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
hasInitialScreenLoaded = false
|
hasInitialScreenLoaded = false
|
||||||
fileTransferCoordinator = null
|
fileTransferCoordinator = null
|
||||||
_ftState.value = FTProgressState()
|
_ftState.value = FTProgressState()
|
||||||
|
_codePage.value = codePageStr
|
||||||
|
clearSearchHighlight()
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -339,6 +446,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
isTlsVerifyCert = effectiveVerify
|
isTlsVerifyCert = effectiveVerify
|
||||||
isTn3270eEnabled = effectiveTn3270e
|
isTn3270eEnabled = effectiveTn3270e
|
||||||
graphicsMode = gMode
|
graphicsMode = gMode
|
||||||
|
codePage = codePageStr
|
||||||
}
|
}
|
||||||
|
|
||||||
config.certificateVerifier = haus.nightmare.lib3270j.tls.TlsCertificateVerifier { chain, _, exception ->
|
config.certificateVerifier = haus.nightmare.lib3270j.tls.TlsCertificateVerifier { chain, _, exception ->
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ object AppSettings {
|
|||||||
|
|
||||||
private const val KEY_VERIFY_CERTS = "verify_certs"
|
private const val KEY_VERIFY_CERTS = "verify_certs"
|
||||||
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
||||||
|
private const val KEY_DEFAULT_CODE_PAGE = "default_code_page"
|
||||||
|
|
||||||
private fun getPrefs(context: Context): SharedPreferences {
|
private fun getPrefs(context: Context): SharedPreferences {
|
||||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
@@ -55,4 +56,12 @@ object AppSettings {
|
|||||||
fun setDefaultGraphicsMode(context: Context, mode: String) {
|
fun setDefaultGraphicsMode(context: Context, mode: String) {
|
||||||
getPrefs(context).edit().putString(KEY_DEFAULT_GRAPHICS_MODE, mode).apply()
|
getPrefs(context).edit().putString(KEY_DEFAULT_GRAPHICS_MODE, mode).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDefaultCodePage(context: Context): String {
|
||||||
|
return getPrefs(context).getString(KEY_DEFAULT_CODE_PAGE, "037") ?: "037"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultCodePage(context: Context, codePage: String) {
|
||||||
|
getPrefs(context).edit().putString(KEY_DEFAULT_CODE_PAGE, codePage).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ data class SavedHost(
|
|||||||
val useTls: Boolean = false,
|
val useTls: Boolean = false,
|
||||||
val tlsVerifyCert: Boolean = true,
|
val tlsVerifyCert: Boolean = true,
|
||||||
val tn3270e: Boolean = true,
|
val tn3270e: Boolean = true,
|
||||||
val graphicsMode: String = "BOTH"
|
val graphicsMode: String = "BOTH",
|
||||||
|
val codePage: String = "037"
|
||||||
) {
|
) {
|
||||||
fun toJson(): JSONObject {
|
fun toJson(): JSONObject {
|
||||||
return JSONObject().apply {
|
return JSONObject().apply {
|
||||||
@@ -34,6 +35,7 @@ data class SavedHost(
|
|||||||
put("tlsVerifyCert", tlsVerifyCert)
|
put("tlsVerifyCert", tlsVerifyCert)
|
||||||
put("tn3270e", tn3270e)
|
put("tn3270e", tn3270e)
|
||||||
put("graphicsMode", graphicsMode)
|
put("graphicsMode", graphicsMode)
|
||||||
|
put("codePage", codePage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +53,8 @@ data class SavedHost(
|
|||||||
useTls = json.optBoolean("useTls", false),
|
useTls = json.optBoolean("useTls", false),
|
||||||
tlsVerifyCert = json.optBoolean("tlsVerifyCert", true),
|
tlsVerifyCert = json.optBoolean("tlsVerifyCert", true),
|
||||||
tn3270e = json.optBoolean("tn3270e", true),
|
tn3270e = json.optBoolean("tn3270e", true),
|
||||||
graphicsMode = json.optString("graphicsMode", "BOTH")
|
graphicsMode = json.optString("graphicsMode", "BOTH"),
|
||||||
|
codePage = json.optString("codePage", "037")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ fun ConnectDialog(
|
|||||||
useTls: Boolean,
|
useTls: Boolean,
|
||||||
tlsVerifyCert: Boolean,
|
tlsVerifyCert: Boolean,
|
||||||
tn3270e: Boolean,
|
tn3270e: Boolean,
|
||||||
graphicsMode: String
|
graphicsMode: String,
|
||||||
|
codePage: String
|
||||||
) -> Unit
|
) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -65,6 +66,7 @@ fun ConnectDialog(
|
|||||||
var tlsVerifyCert by remember { mutableStateOf(true) }
|
var tlsVerifyCert by remember { mutableStateOf(true) }
|
||||||
var tn3270e by remember { mutableStateOf(true) }
|
var tn3270e by remember { mutableStateOf(true) }
|
||||||
var graphicsMode by remember { mutableStateOf("BOTH") }
|
var graphicsMode by remember { mutableStateOf("BOTH") }
|
||||||
|
var codePage by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)) }
|
||||||
|
|
||||||
fun loadProfile(saved: SavedHost) {
|
fun loadProfile(saved: SavedHost) {
|
||||||
selectedHostId = saved.id
|
selectedHostId = saved.id
|
||||||
@@ -79,6 +81,7 @@ fun ConnectDialog(
|
|||||||
tlsVerifyCert = saved.tlsVerifyCert
|
tlsVerifyCert = saved.tlsVerifyCert
|
||||||
tn3270e = saved.tn3270e
|
tn3270e = saved.tn3270e
|
||||||
graphicsMode = saved.graphicsMode
|
graphicsMode = saved.graphicsMode
|
||||||
|
codePage = saved.codePage
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFields() {
|
fun clearFields() {
|
||||||
@@ -94,6 +97,7 @@ fun ConnectDialog(
|
|||||||
tlsVerifyCert = true
|
tlsVerifyCert = true
|
||||||
tn3270e = true
|
tn3270e = true
|
||||||
graphicsMode = "BOTH"
|
graphicsMode = "BOTH"
|
||||||
|
codePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCurrentProfile(): SavedHost? {
|
fun saveCurrentProfile(): SavedHost? {
|
||||||
@@ -113,7 +117,8 @@ fun ConnectDialog(
|
|||||||
useTls = useTls,
|
useTls = useTls,
|
||||||
tlsVerifyCert = tlsVerifyCert,
|
tlsVerifyCert = tlsVerifyCert,
|
||||||
tn3270e = tn3270e,
|
tn3270e = tn3270e,
|
||||||
graphicsMode = graphicsMode
|
graphicsMode = graphicsMode,
|
||||||
|
codePage = codePage
|
||||||
)
|
)
|
||||||
HostStorage.saveHost(context, hostToSave)
|
HostStorage.saveHost(context, hostToSave)
|
||||||
savedHosts = HostStorage.getSavedHosts(context)
|
savedHosts = HostStorage.getSavedHosts(context)
|
||||||
@@ -245,7 +250,8 @@ fun ConnectDialog(
|
|||||||
hostToConn.useTls,
|
hostToConn.useTls,
|
||||||
hostToConn.tlsVerifyCert,
|
hostToConn.tlsVerifyCert,
|
||||||
hostToConn.tn3270e,
|
hostToConn.tn3270e,
|
||||||
hostToConn.graphicsMode
|
hostToConn.graphicsMode,
|
||||||
|
hostToConn.codePage
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
@@ -426,6 +432,90 @@ fun ConnectDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Code Page Selector
|
||||||
|
var codePageMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val codePagesList = listOf(
|
||||||
|
"037" to "037 - US / Canada / Brazil",
|
||||||
|
"1047" to "1047 - IBM Open Systems / z/OS Unix",
|
||||||
|
"500" to "500 - International Latin-1",
|
||||||
|
"273" to "273 - Germany / Austria",
|
||||||
|
"277" to "277 - Denmark / Norway",
|
||||||
|
"278" to "278 - Sweden / Finland",
|
||||||
|
"280" to "280 - Italy",
|
||||||
|
"284" to "284 - Spain / Latin America",
|
||||||
|
"285" to "285 - United Kingdom",
|
||||||
|
"297" to "297 - France",
|
||||||
|
"870" to "870 - Eastern Europe / Latin-2",
|
||||||
|
"871" to "871 - Iceland",
|
||||||
|
"875" to "875 - Greece (Greek)",
|
||||||
|
"1026" to "1026 - Turkey (Turkish)",
|
||||||
|
"1140" to "1140 - US / Canada (Euro €)",
|
||||||
|
"1141" to "1141 - Germany / Austria (Euro €)",
|
||||||
|
"1148" to "1148 - International (Euro €)",
|
||||||
|
"930" to "930 - Japanese Katakana Mixed DBCS",
|
||||||
|
"939" to "939 - Japanese Latin Mixed DBCS",
|
||||||
|
"935" to "935 - Simplified Chinese Mixed DBCS",
|
||||||
|
"937" to "937 - Traditional Chinese Mixed DBCS",
|
||||||
|
"933" to "933 - Korean Mixed DBCS"
|
||||||
|
)
|
||||||
|
val selectedCpLabel = codePagesList.firstOrNull { it.first == codePage }?.second ?: "Code Page $codePage"
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("EBCDIC Code Page:", fontSize = 11.sp, color = Color.Gray)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
color = Color(0xFF2C2D30),
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFF495057)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(38.dp)
|
||||||
|
.clickable { codePageMenuExpanded = true }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = selectedCpLabel,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text("▼", color = Color.Gray, fontSize = 10.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = codePageMenuExpanded,
|
||||||
|
onDismissRequest = { codePageMenuExpanded = false },
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color(0xFF2C2D30))
|
||||||
|
.heightIn(max = 280.dp)
|
||||||
|
) {
|
||||||
|
codePagesList.forEach { (cpId, label) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = if (codePage == cpId) Color(0xFF339AF0) else Color.White,
|
||||||
|
fontWeight = if (codePage == cpId) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
codePage = cpId
|
||||||
|
codePageMenuExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = luName,
|
value = luName,
|
||||||
onValueChange = { luName = it },
|
onValueChange = { luName = it },
|
||||||
@@ -554,7 +644,8 @@ fun ConnectDialog(
|
|||||||
saved.useTls,
|
saved.useTls,
|
||||||
saved.tlsVerifyCert,
|
saved.tlsVerifyCert,
|
||||||
saved.tn3270e,
|
saved.tn3270e,
|
||||||
saved.graphicsMode
|
saved.graphicsMode,
|
||||||
|
saved.codePage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLField
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FieldInspectorDialog(
|
||||||
|
client: Telnet3270Client?,
|
||||||
|
screenVersion: Long,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onJumpToField: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
var fields by remember { mutableStateOf<List<ECLField>>(emptyList()) }
|
||||||
|
var selectedIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
var cols by remember { mutableIntStateOf(80) }
|
||||||
|
var rows by remember { mutableIntStateOf(24) }
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
if (client != null && client.connectionState.isFullSession) {
|
||||||
|
val fieldList = client.fieldList
|
||||||
|
fields = fieldList?.fields ?: emptyList()
|
||||||
|
cols = client.screenBuffer?.cols ?: 80
|
||||||
|
rows = client.screenBuffer?.rows ?: 24
|
||||||
|
} else {
|
||||||
|
fields = emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(screenVersion, client) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.heightIn(max = 680.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"Presentation Space Field Inspector",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${fields.size} fields detected (${rows}x$cols)",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(onClick = { refresh() }) {
|
||||||
|
Text("Refresh", fontSize = 12.sp, color = Color(0xFF339AF0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
if (fields.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"No fields detected or terminal not in 3270 session.",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val horizScroll = rememberScrollState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.horizontalScroll(horizScroll)
|
||||||
|
) {
|
||||||
|
// Header Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color(0xFF25262B))
|
||||||
|
.padding(vertical = 6.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("#", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(30.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Pos", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(42.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Start", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(52.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("End", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(52.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Len", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Prot", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("MDT", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Num", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Hi-Int", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Hidden", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(46.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Pen", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Content Text", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(220.dp), textAlign = TextAlign.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data Rows
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(fields) { idx, field ->
|
||||||
|
val isSelected = (idx == selectedIndex)
|
||||||
|
val startRow = field.startRow + 1
|
||||||
|
val startCol = field.startCol + 1
|
||||||
|
val endRow = field.endRow + 1
|
||||||
|
val endCol = field.endCol + 1
|
||||||
|
|
||||||
|
val displayText = if (field.isHidden) "••••••••" else field.text
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = if (isSelected) Color(0xFF1C7ED6).copy(alpha = 0.35f) else if (idx % 2 == 0) Color(0xFF161719) else Color(0xFF1E1F23),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
selectedIndex = idx
|
||||||
|
onJumpToField(field.dataStart)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("${idx + 1}", color = Color.White, fontSize = 11.sp, modifier = Modifier.width(30.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${field.start}", color = Color.Cyan, fontSize = 10.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(42.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(String.format("%02d/%02d", startRow, startCol), color = Color.LightGray, fontSize = 10.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(52.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(String.format("%02d/%02d", endRow, endCol), color = Color.LightGray, fontSize = 10.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(52.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${field.length}", color = Color.White, fontSize = 10.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(if (field.isProtected) "Y" else "-", color = if (field.isProtected) Color.Gray else Color(0xFF51CF66), fontSize = 10.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(if (field.isModified) "Y" else "-", color = if (field.isModified) Color(0xFFFFB450) else Color.Gray, fontSize = 10.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(if (field.isNumeric) "Y" else "-", color = Color.LightGray, fontSize = 10.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(if (field.isHighIntensity) "Y" else "-", color = if (field.isHighIntensity) Color.White else Color.Gray, fontSize = 10.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(if (field.isHidden) "Y" else "-", color = if (field.isHidden) Color(0xFFFF6B6B) else Color.Gray, fontSize = 10.sp, modifier = Modifier.width(46.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(if (field.isPenSelectable) "Y" else "-", color = if (field.isPenSelectable) Color.Yellow else Color.Gray, fontSize = 10.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(displayText, color = if (field.isProtected) Color(0xFF90CAF9) else Color(0xFFA5D6A7), fontSize = 11.sp, fontFamily = FontFamily.Monospace, maxLines = 1, modifier = Modifier.width(220.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (selectedIndex in fields.indices) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onJumpToField(fields[selectedIndex].dataStart)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||||
|
) {
|
||||||
|
Text("Jump to Field")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Close", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ private val TerminalKeyboardOptions = KeyboardOptions(
|
|||||||
fun FileTransferDialog(
|
fun FileTransferDialog(
|
||||||
initialHostType: String = "TSO",
|
initialHostType: String = "TSO",
|
||||||
ftProgressState: FTProgressState = FTProgressState(),
|
ftProgressState: FTProgressState = FTProgressState(),
|
||||||
|
client: haus.nightmare.lib3270j.Telnet3270Client? = null,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onStartTransfer: (FTConfig) -> Unit,
|
onStartTransfer: (FTConfig) -> Unit,
|
||||||
onCancelTransfer: () -> Unit = {}
|
onCancelTransfer: () -> Unit = {}
|
||||||
@@ -47,6 +48,20 @@ fun FileTransferDialog(
|
|||||||
var lreclStr by remember { mutableStateOf("") }
|
var lreclStr by remember { mutableStateOf("") }
|
||||||
var blksizeStr by remember { mutableStateOf("") }
|
var blksizeStr by remember { mutableStateOf("") }
|
||||||
var overwrite by remember { mutableStateOf(true) }
|
var overwrite by remember { mutableStateOf(true) }
|
||||||
|
var showBrowseDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showBrowseDialog) {
|
||||||
|
HostDirectoryDialog(
|
||||||
|
client = client,
|
||||||
|
initialHostType = hostType,
|
||||||
|
initialQuery = hostFile,
|
||||||
|
onDismiss = { showBrowseDialog = false },
|
||||||
|
onSelectDataset = { selected ->
|
||||||
|
hostFile = selected
|
||||||
|
showBrowseDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -105,6 +120,11 @@ fun FileTransferDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = hostFile,
|
value = hostFile,
|
||||||
onValueChange = { hostFile = it },
|
onValueChange = { hostFile = it },
|
||||||
@@ -112,8 +132,18 @@ fun FileTransferDialog(
|
|||||||
placeholder = { Text(if (hostType == FTConfig.HostType.TSO) "'USER.DATA'" else "PROFILE EXEC A") },
|
placeholder = { Text(if (hostType == FTConfig.HostType.TSO) "'USER.DATA'" else "PROFILE EXEC A") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
if (client != null && client.connectionState.isFullSession) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { showBrowseDialog = true },
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
modifier = Modifier.padding(top = 6.dp)
|
||||||
|
) {
|
||||||
|
Text("Browse...", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = localFile,
|
value = localFile,
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FindDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onFind: (query: String, matchCase: Boolean, forward: Boolean) -> Int,
|
||||||
|
onClearHighlight: () -> Unit
|
||||||
|
) {
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
var matchCase by remember { mutableStateOf(false) }
|
||||||
|
var forward by remember { mutableStateOf(true) }
|
||||||
|
var statusMessage by remember { mutableStateOf("") }
|
||||||
|
var isError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doFind(searchForward: Boolean) {
|
||||||
|
val trimmed = query.trim()
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
statusMessage = "Please enter search text."
|
||||||
|
isError = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val pos = onFind(trimmed, matchCase, searchForward)
|
||||||
|
if (pos >= 0) {
|
||||||
|
val row = (pos / 80) + 1
|
||||||
|
val col = (pos % 80) + 1
|
||||||
|
statusMessage = "Found at position $pos ($row/$col)"
|
||||||
|
isError = false
|
||||||
|
} else {
|
||||||
|
statusMessage = "Text not found: \"$trimmed\""
|
||||||
|
isError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.92f)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Find on Screen",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = {
|
||||||
|
query = it
|
||||||
|
if (statusMessage.isNotBlank()) statusMessage = ""
|
||||||
|
},
|
||||||
|
label = { Text("Find What") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
imeAction = ImeAction.Search
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onSearch = { doFind(forward) }
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.clickable { matchCase = !matchCase }
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = matchCase,
|
||||||
|
onCheckedChange = { matchCase = it }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Match Case", fontSize = 12.sp, color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = forward,
|
||||||
|
onClick = { forward = true },
|
||||||
|
label = { Text("Down ↓", fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = !forward,
|
||||||
|
onClick = { forward = false },
|
||||||
|
label = { Text("Up ↑", fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusMessage.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = statusMessage,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = if (isError) Color(0xFFFF6B6B) else Color(0xFF51CF66)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onClearHighlight()
|
||||||
|
onDismiss()
|
||||||
|
}) {
|
||||||
|
Text("Close", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
OutlinedButton(onClick = {
|
||||||
|
onClearHighlight()
|
||||||
|
statusMessage = ""
|
||||||
|
}) {
|
||||||
|
Text("Clear")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { doFind(forward) },
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||||
|
) {
|
||||||
|
Text("Find Next")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,12 +28,21 @@ fun TwoRowKeyBar(
|
|||||||
onConnectClick: () -> Unit,
|
onConnectClick: () -> Unit,
|
||||||
onDisconnectClick: () -> Unit,
|
onDisconnectClick: () -> Unit,
|
||||||
onFtClick: () -> Unit,
|
onFtClick: () -> Unit,
|
||||||
|
onFieldInspectorClick: () -> Unit = {},
|
||||||
|
onFindClick: () -> Unit = {},
|
||||||
|
onScreenExportClick: () -> Unit = {},
|
||||||
|
onScriptClick: () -> Unit = {},
|
||||||
|
onPrinterSessionClick: () -> Unit = {},
|
||||||
onToggleLightPen: () -> Unit = {},
|
onToggleLightPen: () -> Unit = {},
|
||||||
onSettingsClick: () -> Unit = {},
|
onSettingsClick: () -> Unit = {},
|
||||||
onSendAid: (Int) -> Unit,
|
onSendAid: (Int) -> Unit,
|
||||||
onReset: () -> Unit,
|
onReset: () -> Unit,
|
||||||
onTab: () -> Unit,
|
onTab: () -> Unit,
|
||||||
onBackTab: () -> Unit,
|
onBackTab: () -> Unit,
|
||||||
|
onEraseInput: () -> Unit = {},
|
||||||
|
onAttn: () -> Unit = {},
|
||||||
|
onSysReq: () -> Unit = {},
|
||||||
|
onCursorSelect: () -> Unit = {},
|
||||||
onCursorLeft: () -> Unit = {},
|
onCursorLeft: () -> Unit = {},
|
||||||
onCursorUp: () -> Unit = {},
|
onCursorUp: () -> Unit = {},
|
||||||
onCursorDown: () -> Unit = {},
|
onCursorDown: () -> Unit = {},
|
||||||
@@ -55,7 +64,7 @@ fun TwoRowKeyBar(
|
|||||||
.padding(horizontal = 1.dp, vertical = 1.dp),
|
.padding(horizontal = 1.dp, vertical = 1.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 1. Menu Dropdown Button (Connect/Disconnect/FT/Light Pen/Settings)
|
// 1. Menu Dropdown Button (Connect/Disconnect/FT/Diagnostics/Tools/Settings)
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
KeyButton(
|
KeyButton(
|
||||||
label = "⋮",
|
label = "⋮",
|
||||||
@@ -68,7 +77,9 @@ fun TwoRowKeyBar(
|
|||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = menuExpanded,
|
expanded = menuExpanded,
|
||||||
onDismissRequest = { menuExpanded = false },
|
onDismissRequest = { menuExpanded = false },
|
||||||
modifier = Modifier.background(Color(0xFF2C2D30))
|
modifier = Modifier
|
||||||
|
.background(Color(0xFF2C2D30))
|
||||||
|
.heightIn(max = 420.dp)
|
||||||
) {
|
) {
|
||||||
if (!connectionState.isConnected()) {
|
if (!connectionState.isConnected()) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
@@ -78,6 +89,13 @@ fun TwoRowKeyBar(
|
|||||||
onConnectClick()
|
onConnectClick()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("3287 Printer Session", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onPrinterSessionClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Disconnect", color = Color(0xFFFF6B6B), fontWeight = FontWeight.SemiBold) },
|
text = { Text("Disconnect", color = Color(0xFFFF6B6B), fontWeight = FontWeight.SemiBold) },
|
||||||
@@ -86,13 +104,79 @@ fun TwoRowKeyBar(
|
|||||||
onDisconnectClick()
|
onDisconnectClick()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
HorizontalDivider(color = Color(0xFF3E4044))
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("File Transfer (FT)", color = Color.White, fontWeight = FontWeight.SemiBold) },
|
text = { Text("Find on Screen", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onFindClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Field Inspector", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onFieldInspectorClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("File Transfer (IND\$FILE)", color = Color.White) },
|
||||||
onClick = {
|
onClick = {
|
||||||
menuExpanded = false
|
menuExpanded = false
|
||||||
onFtClick()
|
onFtClick()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Save / Share Screen", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onScreenExportClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Run Keystroke Script", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onScriptClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("3287 Printer Session", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onPrinterSessionClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = Color(0xFF3E4044))
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Attention (Attn)", color = Color.LightGray) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onAttn()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("System Request (SysReq)", color = Color.LightGray) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onSysReq()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Erase Input (ErInp)", color = Color.LightGray) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onEraseInput()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Cursor Select (CurSel)", color = Color.LightGray) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onCursorSelect()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = Color(0xFF3E4044))
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client
|
||||||
|
import haus.nightmare.lib3270j.ft.FTConfig
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HostDirectoryDialog(
|
||||||
|
client: Telnet3270Client?,
|
||||||
|
initialHostType: FTConfig.HostType = FTConfig.HostType.TSO,
|
||||||
|
initialQuery: String = "",
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSelectDataset: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var hostType by remember { mutableStateOf(initialHostType) }
|
||||||
|
var query by remember { mutableStateOf(initialQuery) }
|
||||||
|
var entries by remember { mutableStateOf<List<HostDirectoryEntry>>(emptyList()) }
|
||||||
|
var selectedIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
var statusMessage by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
fun runQuery() {
|
||||||
|
val c = client
|
||||||
|
if (c == null || !c.connectionState.isFullSession) {
|
||||||
|
statusMessage = "Terminal not connected."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val xfer = c.xfer
|
||||||
|
if (xfer == null) {
|
||||||
|
statusMessage = "ECL transfer interface not available."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val q = query.trim()
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
val list = xfer.getCmsDirectory(q)
|
||||||
|
entries = list ?: emptyList()
|
||||||
|
} else {
|
||||||
|
val list = xfer.getTsoDirectory(q)
|
||||||
|
entries = list ?: emptyList()
|
||||||
|
}
|
||||||
|
selectedIndex = -1
|
||||||
|
statusMessage = "${entries.size} entries found."
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseScreen() {
|
||||||
|
val c = client
|
||||||
|
if (c == null || !c.connectionState.isFullSession) {
|
||||||
|
statusMessage = "No active screen to parse."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val xfer = c.xfer
|
||||||
|
if (xfer == null) {
|
||||||
|
statusMessage = "ECL transfer interface not available."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val ps = c.ps
|
||||||
|
val screenText = ps.getString(0, ps.size)
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
val list = xfer.getCmsDirectory(screenText)
|
||||||
|
entries = list ?: emptyList()
|
||||||
|
} else {
|
||||||
|
val list = xfer.getTsoDirectory(screenText)
|
||||||
|
entries = list ?: emptyList()
|
||||||
|
}
|
||||||
|
selectedIndex = -1
|
||||||
|
statusMessage = "Parsed ${entries.size} entries from screen."
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (initialQuery.isNotBlank()) {
|
||||||
|
runQuery()
|
||||||
|
} else {
|
||||||
|
parseScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.heightIn(max = 680.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// Header & Query Bar
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Host Catalog & Directory Browser",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Row {
|
||||||
|
FilterChip(
|
||||||
|
selected = (hostType == FTConfig.HostType.TSO),
|
||||||
|
onClick = { hostType = FTConfig.HostType.TSO },
|
||||||
|
label = { Text("TSO", fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
FilterChip(
|
||||||
|
selected = (hostType == FTConfig.HostType.CMS),
|
||||||
|
onClick = { hostType = FTConfig.HostType.CMS },
|
||||||
|
label = { Text("CMS", fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = { query = it },
|
||||||
|
label = { Text("Query Pattern") },
|
||||||
|
placeholder = { Text(if (hostType == FTConfig.HostType.TSO) "USER.*" else "* EXEC *") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
imeAction = ImeAction.Search
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onSearch = { runQuery() }),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { runQuery() },
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||||
|
) {
|
||||||
|
Text("Query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = statusMessage.ifBlank { "Enter query or parse screen." },
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
TextButton(onClick = { parseScreen() }) {
|
||||||
|
Text("Parse Screen", fontSize = 11.sp, color = Color(0xFF51CF66))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
// Table View
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text("No entries found.", color = Color.Gray, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val horizScroll = rememberScrollState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.horizontalScroll(horizScroll)
|
||||||
|
) {
|
||||||
|
// Header Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color(0xFF25262B))
|
||||||
|
.padding(vertical = 6.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
Text("Filename", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(90.dp))
|
||||||
|
Text("Filetype", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(80.dp))
|
||||||
|
Text("Fm", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Fmt", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Lrecl", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Records", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Date / Time", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(140.dp))
|
||||||
|
} else {
|
||||||
|
Text("Dataset Name", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(220.dp))
|
||||||
|
Text("Volume", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(70.dp))
|
||||||
|
Text("Recfm", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Lrecl", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Blksize", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Tracks", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("Ext", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry Rows
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(entries) { idx, item ->
|
||||||
|
val isSelected = (idx == selectedIndex)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = if (isSelected) Color(0xFF1C7ED6).copy(alpha = 0.35f) else if (idx % 2 == 0) Color(0xFF161719) else Color(0xFF1E1F23),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { selectedIndex = idx }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (item is CMSDirectoryEntry) {
|
||||||
|
Text(item.filename, color = Color.White, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
|
||||||
|
Text(item.filetype, color = Color.Cyan, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(80.dp))
|
||||||
|
Text(item.filemode, color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text(item.recfm, color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${item.lrecl}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${item.numRecords}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${item.date} ${item.time}", color = Color.Gray, fontSize = 10.sp, modifier = Modifier.width(140.dp))
|
||||||
|
} else if (item is TSODirectoryEntry) {
|
||||||
|
Text(item.datasetName, color = Color.White, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(220.dp))
|
||||||
|
Text(item.volume, color = Color.Cyan, fontSize = 11.sp, modifier = Modifier.width(70.dp))
|
||||||
|
Text(item.recfm, color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${item.lrecl}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${item.blksize}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${item.tracksAllocated}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
|
Text("${item.extents}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (selectedIndex in entries.indices) {
|
||||||
|
val selectedName = entries[selectedIndex].name
|
||||||
|
onSelectDataset(selectedName)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = selectedIndex in entries.indices,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
|
) {
|
||||||
|
Text("Select Dataset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ 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,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -121,6 +122,24 @@ 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Light Pen Badge
|
// Light Pen Badge
|
||||||
if (isLightPenMode) {
|
if (isLightPenMode) {
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
|||||||
@@ -0,0 +1,459 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Environment
|
||||||
|
import android.print.PrintAttributes
|
||||||
|
import android.print.PrintDocumentAdapter
|
||||||
|
import android.print.PrintManager
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import haus.nightmare.lib3270j.printer.*
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrinterSessionDialog(
|
||||||
|
initialHost: String = "127.0.0.1",
|
||||||
|
initialPort: Int = 23,
|
||||||
|
initialCodePage: String = "037",
|
||||||
|
initialUseTls: Boolean = false,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
|
var host by remember { mutableStateOf(initialHost.ifBlank { "127.0.0.1" }) }
|
||||||
|
var portStr by remember { mutableStateOf(if (initialPort > 0) initialPort.toString() else "23") }
|
||||||
|
var printerLu by remember { mutableStateOf("") }
|
||||||
|
var assocDisplayLu by remember { mutableStateOf("") }
|
||||||
|
var codePage by remember { mutableStateOf(initialCodePage) }
|
||||||
|
var useTls by remember { mutableStateOf(initialUseTls) }
|
||||||
|
|
||||||
|
var isConnected by remember { mutableStateOf(false) }
|
||||||
|
var sessionStatus by remember { mutableStateOf("Disconnected") }
|
||||||
|
var spoolText by remember { mutableStateOf("") }
|
||||||
|
var pageCount by remember { mutableIntStateOf(0) }
|
||||||
|
var byteCount by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
var printerClient by remember { mutableStateOf<Telnet3270EPClient?>(null) }
|
||||||
|
|
||||||
|
val codePagesList = listOf(
|
||||||
|
"037" to "037 - US / Canada",
|
||||||
|
"1047" to "1047 - Open Systems / Unix",
|
||||||
|
"500" to "500 - International",
|
||||||
|
"273" to "273 - Germany",
|
||||||
|
"277" to "277 - Denmark / Norway",
|
||||||
|
"278" to "278 - Sweden / Finland",
|
||||||
|
"280" to "280 - Italy",
|
||||||
|
"284" to "284 - Spain",
|
||||||
|
"285" to "285 - United Kingdom",
|
||||||
|
"297" to "297 - France",
|
||||||
|
"870" to "870 - Eastern Europe",
|
||||||
|
"1140" to "1140 - US (Euro €)",
|
||||||
|
"1141" to "1141 - Germany (Euro €)",
|
||||||
|
"1148" to "1148 - Intl (Euro €)",
|
||||||
|
"930" to "930 - Japanese Katakana DBCS",
|
||||||
|
"939" to "939 - Japanese Latin DBCS"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun startSession() {
|
||||||
|
val port = portStr.trim().toIntOrNull() ?: 23
|
||||||
|
val config = PrinterConfig(host.trim(), port).apply {
|
||||||
|
this.isUseTls = useTls
|
||||||
|
this.codePage = codePage
|
||||||
|
if (printerLu.isNotBlank()) this.printerLuName = printerLu.trim()
|
||||||
|
if (assocDisplayLu.isNotBlank()) this.associatedDisplayLuName = assocDisplayLu.trim()
|
||||||
|
this.destinationType = PrinterConfig.DestinationType.MEMORY
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = Telnet3270EPClient(config)
|
||||||
|
client.addPrintListener(object : PrintSessionListener {
|
||||||
|
override fun onPrintJobStarted(event: PrintSessionEvent) {
|
||||||
|
sessionStatus = "Printing job: ${event.jobName ?: "Job"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrintJobData(event: PrintSessionEvent) {
|
||||||
|
val captured = client.pd?.capturedText
|
||||||
|
if (captured != null) spoolText = captured
|
||||||
|
byteCount = event.byteCount.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrintJobPageComplete(event: PrintSessionEvent) {
|
||||||
|
pageCount = event.pageNumber
|
||||||
|
byteCount = event.byteCount.toInt()
|
||||||
|
val captured = client.pd?.capturedText
|
||||||
|
if (captured != null) spoolText = captured
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrintJobComplete(event: PrintSessionEvent) {
|
||||||
|
pageCount = event.totalPages
|
||||||
|
byteCount = event.byteCount.toInt()
|
||||||
|
val captured = client.pd?.capturedText
|
||||||
|
if (captured != null) spoolText = captured
|
||||||
|
sessionStatus = "Print Job Complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrinterStatusChanged(event: PrintSessionEvent) {
|
||||||
|
val status = event.statusCode
|
||||||
|
isConnected = (status == PrinterConstants.STATUS_CONNECTED || status == PrinterConstants.STATUS_PRINTING)
|
||||||
|
sessionStatus = when (status) {
|
||||||
|
PrinterConstants.STATUS_CONNECTED -> "Connected"
|
||||||
|
PrinterConstants.STATUS_CONNECTING -> "Connecting..."
|
||||||
|
PrinterConstants.STATUS_PRINTING -> "Printing..."
|
||||||
|
else -> "Disconnected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrinterError(event: PrintSessionEvent) {
|
||||||
|
sessionStatus = "Error: ${event.message ?: "Code ${event.statusCode}"}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
printerClient = client
|
||||||
|
sessionStatus = "Connecting..."
|
||||||
|
val ok = client.connect()
|
||||||
|
if (!ok) {
|
||||||
|
sessionStatus = "Failed to initiate printer connection."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopSession() {
|
||||||
|
printerClient?.disconnect()
|
||||||
|
printerClient = null
|
||||||
|
isConnected = false
|
||||||
|
sessionStatus = "Disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
printerClient?.disconnect()
|
||||||
|
printerClient = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun printViaAndroidService() {
|
||||||
|
if (spoolText.isBlank()) {
|
||||||
|
Toast.makeText(context, "No printer spool to print.", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val printManager = context.getSystemService(Context.PRINT_SERVICE) as? PrintManager
|
||||||
|
if (printManager != null) {
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
val printAdapter: PrintDocumentAdapter = webView.createPrintDocumentAdapter("3287_Print_Job")
|
||||||
|
printManager.print("3287 Print Job", printAdapter, PrintAttributes.Builder().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val htmlDoc = "<html><body style='font-family:monospace; font-size:12px; white-space:pre;'>${spoolText.replace("<", "<").replace(">", ">")}</body></html>"
|
||||||
|
webView.loadDataWithBaseURL(null, htmlDoc, "text/html", "UTF-8", null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "Print failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveSpoolToFile() {
|
||||||
|
if (spoolText.isBlank()) {
|
||||||
|
Toast.makeText(context, "Spool is empty.", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
|
val file = File(downloadsDir, "3287_spool_$timeStamp.txt")
|
||||||
|
file.writeText(spoolText)
|
||||||
|
Toast.makeText(context, "Saved to Downloads/${file.name}", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "Save failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareSpool() {
|
||||||
|
if (spoolText.isBlank()) {
|
||||||
|
Toast.makeText(context, "Spool is empty.", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "3287 Printer Spool")
|
||||||
|
putExtra(Intent.EXTRA_TEXT, spoolText)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(intent, "Share 3287 Spool"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "Share failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.heightIn(max = 700.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"IBM 3287 Printer Session",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"TN3270E SCS / 3270 DS Print Emulation",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
color = if (isConnected) Color(0xFF51CF66).copy(alpha = 0.2f) else Color(0xFFFF6B6B).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = sessionStatus,
|
||||||
|
color = if (isConnected) Color(0xFF51CF66) else Color(0xFFFF6B6B),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
// Config Row 1: Host & Port
|
||||||
|
if (!isConnected) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = host,
|
||||||
|
onValueChange = { host = it },
|
||||||
|
label = { Text("Host") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(2.5f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = portStr,
|
||||||
|
onValueChange = { portStr = it },
|
||||||
|
label = { Text("Port") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config Row 2: Printer LU & Assoc Display LU
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = printerLu,
|
||||||
|
onValueChange = { printerLu = it },
|
||||||
|
label = { Text("Printer LU (Opt)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = assocDisplayLu,
|
||||||
|
onValueChange = { assocDisplayLu = it },
|
||||||
|
label = { Text("Display LU (Opt)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code Page & TLS Row
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
var cpExpanded by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
OutlinedButton(onClick = { cpExpanded = true }) {
|
||||||
|
Text("Code Page: CP$codePage", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = cpExpanded,
|
||||||
|
onDismissRequest = { cpExpanded = false },
|
||||||
|
modifier = Modifier.background(Color(0xFF2C2D30)).heightIn(max = 240.dp)
|
||||||
|
) {
|
||||||
|
codePagesList.forEach { (cpId, label) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(label, fontSize = 12.sp, color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
codePage = cpId
|
||||||
|
cpExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.clickable { useTls = !useTls }
|
||||||
|
) {
|
||||||
|
Checkbox(checked = useTls, onCheckedChange = { useTls = it })
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("TLS", fontSize = 12.sp, color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { startSession() },
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
|
) {
|
||||||
|
Text("Connect Printer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Pages: $pageCount | Bytes: $byteCount",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.LightGray,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { stopSession() },
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC92A2A))
|
||||||
|
) {
|
||||||
|
Text("Disconnect Printer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
// Spool Display Box
|
||||||
|
Text("Print Spool Buffer:", fontSize = 12.sp, color = Color.Gray)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.background(Color(0xFF141517), shape = RoundedCornerShape(8.dp))
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
if (spoolText.isBlank()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("No printed output spooled yet.", color = Color.DarkGray, fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = spoolText,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color(0xFFA5D6A7),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spool Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Close", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
spoolText = ""
|
||||||
|
pageCount = 0
|
||||||
|
byteCount = 0
|
||||||
|
printerClient?.pd?.resetCapture()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Clear", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(spoolText))
|
||||||
|
Toast.makeText(context, "Copied spool to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Copy", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { saveSpoolToFile() },
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||||
|
) {
|
||||||
|
Text("Save", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { printViaAndroidService() },
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
|
) {
|
||||||
|
Text("Print 🖨️", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
||||||
|
import haus.nightmare.lib3270j.graphics.ProgramSymbolManager
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenExporterDialog(
|
||||||
|
screenBuffer: ScreenBuffer?,
|
||||||
|
rows: Int,
|
||||||
|
cols: Int,
|
||||||
|
programSymbolManager: ProgramSymbolManager? = null,
|
||||||
|
graphicsPlane: GraphicsPlane? = null,
|
||||||
|
maskHidden: Boolean = true,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
|
var exportFormat by remember { mutableStateOf("TEXT") } // TEXT, HTML, PNG
|
||||||
|
var statusMessage by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val plainTextContent = remember(screenBuffer) {
|
||||||
|
generatePlainText(screenBuffer, rows, cols, maskHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
val htmlContent = remember(screenBuffer) {
|
||||||
|
generateHtml(screenBuffer, rows, cols, maskHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareText(text: String, isHtml: Boolean) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = if (isHtml) "text/html" else "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "3270 Screen Capture")
|
||||||
|
putExtra(Intent.EXTRA_TEXT, text)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(intent, "Share 3270 Screen"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
statusMessage = "Share failed: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sharePng(bmp: Bitmap) {
|
||||||
|
try {
|
||||||
|
val cacheDir = File(context.cacheDir, "exports").apply { mkdirs() }
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
|
val file = File(cacheDir, "screen_$timeStamp.png")
|
||||||
|
FileOutputStream(file).use { out ->
|
||||||
|
bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
|
}
|
||||||
|
val uri: Uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "image/png"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(intent, "Share Screen PNG"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
statusMessage = "PNG Share failed: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveFileToStorage(format: String): String {
|
||||||
|
return try {
|
||||||
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
||||||
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
|
when (format) {
|
||||||
|
"HTML" -> {
|
||||||
|
val file = File(downloadsDir, "3270_screen_$timeStamp.html")
|
||||||
|
file.writeText(htmlContent)
|
||||||
|
"Saved to Downloads/${file.name}"
|
||||||
|
}
|
||||||
|
"PNG" -> {
|
||||||
|
val bmp = generateBitmap(screenBuffer, rows, cols, programSymbolManager, graphicsPlane, maskHidden)
|
||||||
|
val file = File(downloadsDir, "3270_screen_$timeStamp.png")
|
||||||
|
FileOutputStream(file).use { out ->
|
||||||
|
bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
|
}
|
||||||
|
"Saved to Downloads/${file.name}"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val file = File(downloadsDir, "3270_screen_$timeStamp.txt")
|
||||||
|
file.writeText(plainTextContent)
|
||||||
|
"Saved to Downloads/${file.name}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"Save failed: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.heightIn(max = 680.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Export / Share Screen",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Row {
|
||||||
|
FilterChip(
|
||||||
|
selected = (exportFormat == "TEXT"),
|
||||||
|
onClick = { exportFormat = "TEXT" },
|
||||||
|
label = { Text("Plain Text", fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
FilterChip(
|
||||||
|
selected = (exportFormat == "HTML"),
|
||||||
|
onClick = { exportFormat = "HTML" },
|
||||||
|
label = { Text("HTML", fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
FilterChip(
|
||||||
|
selected = (exportFormat == "PNG"),
|
||||||
|
onClick = { exportFormat = "PNG" },
|
||||||
|
label = { Text("PNG", fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
// Preview Box
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.background(Color(0xFF141517), shape = RoundedCornerShape(8.dp))
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
if (exportFormat == "PNG") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("🖼️ PNG Image Export", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text("Full resolution bitmap with 3270 styling and graphics plane.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val textToShow = if (exportFormat == "HTML") htmlContent else plainTextContent
|
||||||
|
Text(
|
||||||
|
text = textToShow,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color(0xFFA5D6A7),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusMessage.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = statusMessage,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF51CF66)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Close", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
if (exportFormat != "PNG") {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
val content = if (exportFormat == "HTML") htmlContent else plainTextContent
|
||||||
|
clipboardManager.setText(AnnotatedString(content))
|
||||||
|
statusMessage = "Copied to clipboard."
|
||||||
|
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Copy", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val msg = saveFileToStorage(exportFormat)
|
||||||
|
statusMessage = msg
|
||||||
|
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||||
|
) {
|
||||||
|
Text("Save File", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
when (exportFormat) {
|
||||||
|
"PNG" -> {
|
||||||
|
val bmp = generateBitmap(screenBuffer, rows, cols, programSymbolManager, graphicsPlane, maskHidden)
|
||||||
|
sharePng(bmp)
|
||||||
|
}
|
||||||
|
"HTML" -> shareText(htmlContent, true)
|
||||||
|
else -> shareText(plainTextContent, false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
|
) {
|
||||||
|
Text("Share 📤", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, maskHidden: Boolean): String {
|
||||||
|
if (screenBuffer == null) return ""
|
||||||
|
val sb = StringBuilder()
|
||||||
|
var currentHidden = false
|
||||||
|
|
||||||
|
for (r in 0 until rows) {
|
||||||
|
val rowSb = StringBuilder()
|
||||||
|
for (c in 0 until cols) {
|
||||||
|
val addr = r * cols + c
|
||||||
|
val cell = screenBuffer.getCell(addr)
|
||||||
|
if (cell.fa.toInt() != 0) {
|
||||||
|
val faVal = cell.fa.toInt() and 0xFF
|
||||||
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
|
rowSb.append(' ')
|
||||||
|
} else {
|
||||||
|
if (currentHidden && maskHidden) {
|
||||||
|
rowSb.append(' ')
|
||||||
|
} else {
|
||||||
|
val ch = cell.ucs4
|
||||||
|
rowSb.append(if (ch in ' '..'~' || ch > '\u007F') ch else ' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(rowSb.toString().trimEnd()).append("\n")
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, maskHidden: Boolean): String {
|
||||||
|
if (screenBuffer == null) return "<html><body><pre></pre></body></html>"
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append("<!DOCTYPE html>\n<html>\n<head>\n")
|
||||||
|
sb.append("<meta charset=\"UTF-8\">\n")
|
||||||
|
sb.append("<style>\n")
|
||||||
|
sb.append("body { background-color: #0A0A0A; color: #00FF66; font-family: monospace; font-size: 14px; margin: 20px; }\n")
|
||||||
|
sb.append("pre { line-height: 1.2; font-family: monospace; }\n")
|
||||||
|
sb.append(".c-green { color: #00FF66; }\n")
|
||||||
|
sb.append(".c-white { color: #FFFFFF; }\n")
|
||||||
|
sb.append(".c-blue { color: #3399FF; }\n")
|
||||||
|
sb.append(".c-red { color: #FF3333; }\n")
|
||||||
|
sb.append(".c-yellow { color: #FFFF33; }\n")
|
||||||
|
sb.append(".c-turquoise { color: #00E5FF; }\n")
|
||||||
|
sb.append(".c-pink { color: #FF66CC; }\n")
|
||||||
|
sb.append("</style>\n</head>\n<body>\n<pre>\n")
|
||||||
|
|
||||||
|
var currentHidden = false
|
||||||
|
var currentProt = false
|
||||||
|
var currentHi = false
|
||||||
|
|
||||||
|
for (r in 0 until rows) {
|
||||||
|
for (c in 0 until cols) {
|
||||||
|
val addr = r * cols + c
|
||||||
|
val cell = screenBuffer.getCell(addr)
|
||||||
|
if (cell.fa.toInt() != 0) {
|
||||||
|
val faVal = cell.fa.toInt() and 0xFF
|
||||||
|
currentProt = (faVal and FA_PROTECT) != 0
|
||||||
|
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||||
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
|
sb.append(' ')
|
||||||
|
} else {
|
||||||
|
if (currentHidden && maskHidden) {
|
||||||
|
sb.append(' ')
|
||||||
|
} else {
|
||||||
|
val ch = cell.ucs4
|
||||||
|
val colorClass = when {
|
||||||
|
currentHi -> "c-white"
|
||||||
|
currentProt -> "c-blue"
|
||||||
|
else -> "c-green"
|
||||||
|
}
|
||||||
|
val escChar = when (ch) {
|
||||||
|
'<' -> "<"
|
||||||
|
'>' -> ">"
|
||||||
|
'&' -> "&"
|
||||||
|
'"' -> """
|
||||||
|
else -> if (ch in ' '..'~' || ch > '\u007F') ch.toString() else " "
|
||||||
|
}
|
||||||
|
sb.append("<span class=\"$colorClass\">$escChar</span>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("</pre>\n</body>\n</html>")
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateBitmap(
|
||||||
|
screenBuffer: ScreenBuffer?,
|
||||||
|
rows: Int,
|
||||||
|
cols: Int,
|
||||||
|
programSymbolManager: ProgramSymbolManager?,
|
||||||
|
graphicsPlane: GraphicsPlane?,
|
||||||
|
maskHidden: Boolean
|
||||||
|
): Bitmap {
|
||||||
|
val charWidth = 14
|
||||||
|
val charHeight = 24
|
||||||
|
val width = cols * charWidth
|
||||||
|
val height = rows * charHeight
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
|
||||||
|
// Fill background
|
||||||
|
val bgPaint = Paint().apply {
|
||||||
|
color = android.graphics.Color.parseColor("#0A0A0A")
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), bgPaint)
|
||||||
|
|
||||||
|
if (screenBuffer == null) return bitmap
|
||||||
|
|
||||||
|
val textPaint = Paint().apply {
|
||||||
|
isAntiAlias = true
|
||||||
|
typeface = Typeface.MONOSPACE
|
||||||
|
textSize = 18f
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentHidden = false
|
||||||
|
var currentProt = false
|
||||||
|
var currentHi = false
|
||||||
|
|
||||||
|
for (r in 0 until rows) {
|
||||||
|
for (c in 0 until cols) {
|
||||||
|
val addr = r * cols + c
|
||||||
|
val cell = screenBuffer.getCell(addr)
|
||||||
|
val left = (c * charWidth).toFloat()
|
||||||
|
val top = (r * charHeight).toFloat()
|
||||||
|
|
||||||
|
if (cell.fa.toInt() != 0) {
|
||||||
|
val faVal = cell.fa.toInt() and 0xFF
|
||||||
|
currentProt = (faVal and FA_PROTECT) != 0
|
||||||
|
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||||
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
|
} else if (!currentHidden || !maskHidden) {
|
||||||
|
val ch = cell.ucs4
|
||||||
|
if (ch in ' '..'~' || ch > '\u007F') {
|
||||||
|
textPaint.color = when {
|
||||||
|
currentHi -> android.graphics.Color.WHITE
|
||||||
|
currentProt -> android.graphics.Color.parseColor("#3399FF")
|
||||||
|
else -> android.graphics.Color.parseColor("#00FF66")
|
||||||
|
}
|
||||||
|
canvas.drawText(ch.toString(), left + 2f, top + 18f, textPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay vector graphics plane if present
|
||||||
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
|
val rgb = graphicsPlane.rgbBuffer
|
||||||
|
val gWidth = graphicsPlane.canvasWidth
|
||||||
|
val gHeight = graphicsPlane.canvasHeight
|
||||||
|
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||||
|
val gfxBmp = Bitmap.createBitmap(rgb, gWidth, gHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val scaledGfx = Bitmap.createScaledBitmap(gfxBmp, width, height, true)
|
||||||
|
canvas.drawBitmap(scaledGfx, 0f, 0f, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScriptDialog(
|
||||||
|
initialScript: String = "",
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onExecuteScript: (script: String, onStatus: (String) -> Unit, onComplete: (String?) -> Unit) -> Unit
|
||||||
|
) {
|
||||||
|
var scriptText by remember { mutableStateOf(initialScript.ifBlank { "# Sample 3270 Script\nLOGON APPLID(TSO)[enter]\n[wait]\nUSERID[tab]PASSWORD[enter]" }) }
|
||||||
|
var isExecuting by remember { mutableStateOf(false) }
|
||||||
|
var executionStatus by remember { mutableStateOf("") }
|
||||||
|
var executionError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
fun insertToken(token: String) {
|
||||||
|
scriptText += token
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { if (!isExecuting) onDismiss() },
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.heightIn(max = 680.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Run Keystroke Script / Macro",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"ECL Mnemonic Tokens",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
// Mnemonic Quick Insert Chips Row 1
|
||||||
|
Text("Insert Mnemonic Keys:", fontSize = 11.sp, color = Color.LightGray)
|
||||||
|
val tokenScroll1 = rememberScrollState()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(tokenScroll1),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
"[enter]", "[tab]", "[backtab]", "[home]", "[clear]",
|
||||||
|
"[reset]", "[eraseeof]", "[eraseinpt]", "[attn]", "[sysreq]"
|
||||||
|
).forEach { tok ->
|
||||||
|
SuggestionChip(
|
||||||
|
onClick = { insertToken(tok) },
|
||||||
|
label = { Text(tok, fontSize = 10.sp, fontFamily = FontFamily.Monospace) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mnemonic Quick Insert Chips Row 2 (PF Keys)
|
||||||
|
val tokenScroll2 = rememberScrollState()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(tokenScroll2),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
(1..24).forEach { pf ->
|
||||||
|
val tok = "[pf$pf]"
|
||||||
|
SuggestionChip(
|
||||||
|
onClick = { insertToken(tok) },
|
||||||
|
label = { Text("PF$pf", fontSize = 10.sp, fontFamily = FontFamily.Monospace) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script Editor Box
|
||||||
|
OutlinedTextField(
|
||||||
|
value = scriptText,
|
||||||
|
onValueChange = { scriptText = it },
|
||||||
|
label = { Text("Script Text / Mnemonic Commands") },
|
||||||
|
placeholder = { Text("Type characters or tokens like [enter], [pf3], etc.") },
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Ascii
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace, fontSize = 12.sp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Status or Error
|
||||||
|
if (isExecuting) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||||
|
Text(
|
||||||
|
text = executionStatus.ifBlank { "Executing script..." },
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF339AF0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (executionError != null) {
|
||||||
|
Text(
|
||||||
|
text = "Error: $executionError",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFFFF6B6B)
|
||||||
|
)
|
||||||
|
} else if (executionStatus.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = executionStatus,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color(0xFF51CF66)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
enabled = !isExecuting
|
||||||
|
) {
|
||||||
|
Text("Close", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { scriptText = "" },
|
||||||
|
enabled = !isExecuting
|
||||||
|
) {
|
||||||
|
Text("Clear")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (scriptText.isNotBlank()) {
|
||||||
|
isExecuting = true
|
||||||
|
executionError = null
|
||||||
|
executionStatus = "Starting..."
|
||||||
|
onExecuteScript(
|
||||||
|
scriptText,
|
||||||
|
{ status -> executionStatus = status },
|
||||||
|
{ error ->
|
||||||
|
isExecuting = false
|
||||||
|
if (error != null) {
|
||||||
|
executionError = error
|
||||||
|
} else {
|
||||||
|
executionStatus = "Script completed successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !isExecuting && scriptText.isNotBlank(),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
|
) {
|
||||||
|
Text("Execute Script ▶")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package haus.nightmare.a3270.ui
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -24,14 +25,16 @@ fun SettingsDialog(
|
|||||||
initialHapticFeedback: Boolean = true,
|
initialHapticFeedback: Boolean = true,
|
||||||
initialVerifyCerts: Boolean = true,
|
initialVerifyCerts: Boolean = true,
|
||||||
initialDefaultGraphicsMode: String = "BOTH",
|
initialDefaultGraphicsMode: String = "BOTH",
|
||||||
|
initialDefaultCodePage: String = "037",
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean, verifyCerts: Boolean, defaultGraphicsMode: String) -> Unit
|
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean, verifyCerts: Boolean, defaultGraphicsMode: String, defaultCodePage: String) -> Unit
|
||||||
) {
|
) {
|
||||||
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
|
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
|
||||||
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
|
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
|
||||||
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
|
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
|
||||||
var verifyCerts by remember { mutableStateOf(initialVerifyCerts) }
|
var verifyCerts by remember { mutableStateOf(initialVerifyCerts) }
|
||||||
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
||||||
|
var defaultCodePage by remember { mutableStateOf(initialDefaultCodePage) }
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -242,6 +245,104 @@ fun SettingsDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Setting 6: Default EBCDIC Code Page
|
||||||
|
var codePageMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val codePagesList = listOf(
|
||||||
|
"037" to "037 - US / Canada / Brazil",
|
||||||
|
"1047" to "1047 - IBM Open Systems / z/OS Unix",
|
||||||
|
"500" to "500 - International Latin-1",
|
||||||
|
"273" to "273 - Germany / Austria",
|
||||||
|
"277" to "277 - Denmark / Norway",
|
||||||
|
"278" to "278 - Sweden / Finland",
|
||||||
|
"280" to "280 - Italy",
|
||||||
|
"284" to "284 - Spain / Latin America",
|
||||||
|
"285" to "285 - United Kingdom",
|
||||||
|
"297" to "297 - France",
|
||||||
|
"870" to "870 - Eastern Europe / Latin-2",
|
||||||
|
"871" to "871 - Iceland",
|
||||||
|
"875" to "875 - Greece (Greek)",
|
||||||
|
"1026" to "1026 - Turkey (Turkish)",
|
||||||
|
"1140" to "1140 - US / Canada (Euro €)",
|
||||||
|
"1141" to "1141 - Germany / Austria (Euro €)",
|
||||||
|
"1148" to "1148 - International (Euro €)",
|
||||||
|
"930" to "930 - Japanese Katakana Mixed DBCS",
|
||||||
|
"939" to "939 - Japanese Latin Mixed DBCS",
|
||||||
|
"935" to "935 - Simplified Chinese Mixed DBCS",
|
||||||
|
"937" to "937 - Traditional Chinese Mixed DBCS",
|
||||||
|
"933" to "933 - Korean Mixed DBCS"
|
||||||
|
)
|
||||||
|
val selectedCpLabel = codePagesList.firstOrNull { it.first == defaultCodePage }?.second ?: "Code Page $defaultCodePage"
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = "Default EBCDIC Code Page",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Character encoding translation table for new sessions.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
color = Color(0xFF2C2D30),
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFF495057)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(38.dp)
|
||||||
|
.clickable { codePageMenuExpanded = true }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = selectedCpLabel,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text("▼", color = Color.Gray, fontSize = 10.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = codePageMenuExpanded,
|
||||||
|
onDismissRequest = { codePageMenuExpanded = false },
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color(0xFF2C2D30))
|
||||||
|
.heightIn(max = 280.dp)
|
||||||
|
) {
|
||||||
|
codePagesList.forEach { (cpId, label) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = if (defaultCodePage == cpId) Color(0xFF339AF0) else Color.White,
|
||||||
|
fontWeight = if (defaultCodePage == cpId) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
defaultCodePage = cpId
|
||||||
|
codePageMenuExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Action Buttons
|
// Action Buttons
|
||||||
@@ -255,7 +356,7 @@ fun SettingsDialog(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(maskHiddenInput, cursorBlink, hapticFeedback, verifyCerts, defaultGraphicsMode)
|
onSave(maskHiddenInput, cursorBlink, hapticFeedback, verifyCerts, defaultGraphicsMode, defaultCodePage)
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ fun TerminalView(
|
|||||||
isLightPenMode: Boolean = false,
|
isLightPenMode: Boolean = false,
|
||||||
maskHiddenFields: Boolean = true,
|
maskHiddenFields: Boolean = true,
|
||||||
blinkCursor: Boolean = true,
|
blinkCursor: Boolean = true,
|
||||||
|
searchHighlightAddr: Int = -1,
|
||||||
|
searchHighlightLen: Int = 0,
|
||||||
onTapAddress: (Int) -> Unit,
|
onTapAddress: (Int) -> Unit,
|
||||||
onLightPenSelect: ((Int) -> Unit)? = null,
|
onLightPenSelect: ((Int) -> Unit)? = null,
|
||||||
onGraphicTouch: ((Int, Int) -> Unit)? = null,
|
onGraphicTouch: ((Int, Int) -> Unit)? = null,
|
||||||
@@ -314,7 +316,16 @@ fun TerminalView(
|
|||||||
|
|
||||||
// Draw selection box highlight if selected
|
// Draw selection box highlight if selected
|
||||||
val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
||||||
if (isSelected) {
|
val isSearchHighlighted = searchHighlightAddr >= 0 && searchHighlightLen > 0 &&
|
||||||
|
addr >= searchHighlightAddr && addr < (searchHighlightAddr + searchHighlightLen)
|
||||||
|
|
||||||
|
if (isSearchHighlighted) {
|
||||||
|
drawRect(
|
||||||
|
color = Color(0x99FFC107), // Amber / Gold highlight for search matches
|
||||||
|
topLeft = Offset(left, top),
|
||||||
|
size = Size(cellWidth, cellHeight)
|
||||||
|
)
|
||||||
|
} else if (isSelected) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color(0x773399FF),
|
color = Color(0x773399FF),
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.a3270.storage.SavedHost
|
||||||
|
import haus.nightmare.lib3270j.charset.CodePageRegistry
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class CodePageAndStorageTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSavedHostCodePageSerialization() {
|
||||||
|
val host = SavedHost(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
name = "Test Mainframe",
|
||||||
|
host = "192.168.1.100",
|
||||||
|
port = 23,
|
||||||
|
model = 4,
|
||||||
|
luName = "LU3270",
|
||||||
|
autoConnect = true,
|
||||||
|
hostType = "TSO",
|
||||||
|
useTls = true,
|
||||||
|
tlsVerifyCert = false,
|
||||||
|
tn3270e = true,
|
||||||
|
graphicsMode = "BOTH",
|
||||||
|
codePage = "1047"
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = host.toJson()
|
||||||
|
assertEquals("1047", json.getString("codePage"))
|
||||||
|
assertEquals("Test Mainframe", json.getString("name"))
|
||||||
|
assertTrue(json.getBoolean("autoConnect"))
|
||||||
|
|
||||||
|
val deserialized = SavedHost.fromJson(json)
|
||||||
|
assertEquals(host.id, deserialized.id)
|
||||||
|
assertEquals(host.name, deserialized.name)
|
||||||
|
assertEquals(host.host, deserialized.host)
|
||||||
|
assertEquals(host.port, deserialized.port)
|
||||||
|
assertEquals(host.model, deserialized.model)
|
||||||
|
assertEquals(host.codePage, deserialized.codePage)
|
||||||
|
assertEquals(host.graphicsMode, deserialized.graphicsMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSavedHostDefaultCodePageFallback() {
|
||||||
|
// Old JSON without codePage field
|
||||||
|
val json = org.json.JSONObject().apply {
|
||||||
|
put("name", "Legacy")
|
||||||
|
put("host", "mvs.local")
|
||||||
|
}
|
||||||
|
val deserialized = SavedHost.fromJson(json)
|
||||||
|
assertEquals("037", deserialized.codePage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCodePageRegistryResolution() {
|
||||||
|
val cp037 = CodePageRegistry.getCodePage("037")
|
||||||
|
assertNotNull(cp037)
|
||||||
|
assertEquals("037", cp037.codePageId)
|
||||||
|
|
||||||
|
val cp1047 = CodePageRegistry.getCodePage("1047")
|
||||||
|
assertNotNull(cp1047)
|
||||||
|
assertEquals("1047", cp1047.codePageId)
|
||||||
|
|
||||||
|
val cp930 = CodePageRegistry.getCodePage("930")
|
||||||
|
assertNotNull(cp930)
|
||||||
|
assertTrue(cp930.isDBCS)
|
||||||
|
|
||||||
|
val cp1140 = CodePageRegistry.getCodePage("1140")
|
||||||
|
assertNotNull(cp1140)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEbcdicTranslatorCodePageSwitching() {
|
||||||
|
val translator = EbcdicTranslator("037")
|
||||||
|
assertEquals("037", translator.codePageId)
|
||||||
|
|
||||||
|
val translator1047 = EbcdicTranslator("1047")
|
||||||
|
assertEquals("1047", translator1047.codePageId)
|
||||||
|
|
||||||
|
val model = TerminalModel.IBM_3279_2
|
||||||
|
val sb = ScreenBuffer(model, translator)
|
||||||
|
assertEquals('A', translator.ebcdicToUnicode(0xC1))
|
||||||
|
sb.getCell(0).ec = 0xC1.toByte()
|
||||||
|
sb.translateToUnicode()
|
||||||
|
assertEquals('A', sb.getCell(0).ucs4)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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.ECLFieldList
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLPS
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class EclAndSearchTest {
|
||||||
|
|
||||||
|
private lateinit var screenBuffer: ScreenBuffer
|
||||||
|
private lateinit var translator: EbcdicTranslator
|
||||||
|
private lateinit var inputProcessor: haus.nightmare.lib3270j.input.InputProcessor
|
||||||
|
private lateinit var ps: ECLPS
|
||||||
|
private lateinit var fieldList: ECLFieldList
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val model = TerminalModel.IBM_3279_2 // 24x80
|
||||||
|
translator = EbcdicTranslator()
|
||||||
|
screenBuffer = ScreenBuffer(model, translator)
|
||||||
|
inputProcessor = haus.nightmare.lib3270j.input.InputProcessor(screenBuffer, translator, null)
|
||||||
|
ps = ECLPS(screenBuffer, inputProcessor, translator)
|
||||||
|
fieldList = ps.fieldList
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPresentationSpaceSearchForwardAndBackward() {
|
||||||
|
val text = "USERID ===> IBMUSER"
|
||||||
|
for (i in text.indices) {
|
||||||
|
screenBuffer.getCell(100 + i).ucs4 = text[i]
|
||||||
|
screenBuffer.getCell(100 + i).ec = translator.unicodeToEbcdic(text[i]).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Forward for "IBMUSER" from row 0 col 0
|
||||||
|
val foundPos = ps.searchString("IBMUSER", 0, 0, ECLConstants.SEARCH_FORWARD, true)
|
||||||
|
assertEquals(100 + text.indexOf("IBMUSER"), foundPos)
|
||||||
|
|
||||||
|
// Case-sensitive search failure
|
||||||
|
val notFound = ps.searchString("ibmuser", 0, 0, ECLConstants.SEARCH_FORWARD, false)
|
||||||
|
assertEquals(-1, notFound)
|
||||||
|
|
||||||
|
// Case-insensitive search match
|
||||||
|
val foundCaseInsensitive = ps.searchString("ibmuser", 0, 0, ECLConstants.SEARCH_FORWARD, true)
|
||||||
|
assertEquals(100 + text.indexOf("IBMUSER"), foundCaseInsensitive)
|
||||||
|
|
||||||
|
// Search Backward from bottom of screen
|
||||||
|
val foundBackward = ps.searchString("USERID", 23, 79, ECLConstants.SEARCH_BACKWARD, true)
|
||||||
|
assertEquals(100, foundBackward)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFieldListExtractionAndFlags() {
|
||||||
|
// Position 0: Protected label
|
||||||
|
screenBuffer.setCellFA(0, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
val label = "COMMAND ===>"
|
||||||
|
for (i in label.indices) {
|
||||||
|
screenBuffer.getCell(1 + i).ucs4 = label[i]
|
||||||
|
screenBuffer.getCell(1 + i).ec = translator.unicodeToEbcdic(label[i]).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position 20: Unprotected input field with MDT
|
||||||
|
screenBuffer.setCellFA(20, (FA_PRINTABLE or FA_MODIFY).toByte())
|
||||||
|
val cmd = "TSO TEST"
|
||||||
|
for (i in cmd.indices) {
|
||||||
|
screenBuffer.getCell(21 + i).ucs4 = cmd[i]
|
||||||
|
screenBuffer.getCell(21 + i).ec = translator.unicodeToEbcdic(cmd[i]).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position 40: Hidden field (zero intensity)
|
||||||
|
screenBuffer.setCellFA(40, (FA_PRINTABLE or FA_PROTECT or FA_INT_ZERO_NSEL).toByte())
|
||||||
|
|
||||||
|
fieldList.refresh()
|
||||||
|
val fields = fieldList.fields
|
||||||
|
assertEquals(3, fields.size)
|
||||||
|
|
||||||
|
// Field 0: Label
|
||||||
|
val f0 = fields[0]
|
||||||
|
assertTrue(f0.isProtected)
|
||||||
|
assertFalse(f0.isModified)
|
||||||
|
assertEquals(0, f0.start)
|
||||||
|
assertEquals(1, f0.dataStart)
|
||||||
|
assertTrue(f0.text.startsWith("COMMAND ===>"))
|
||||||
|
|
||||||
|
// Field 1: Input command
|
||||||
|
val f1 = fields[1]
|
||||||
|
assertFalse(f1.isProtected)
|
||||||
|
assertTrue(f1.isModified)
|
||||||
|
assertEquals(20, f1.start)
|
||||||
|
assertEquals(21, f1.dataStart)
|
||||||
|
assertTrue(f1.text.startsWith("TSO TEST"))
|
||||||
|
|
||||||
|
// Field 2: Hidden field
|
||||||
|
val f2 = fields[2]
|
||||||
|
assertTrue(f2.isHidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor
|
||||||
|
import haus.nightmare.lib3270j.printer.PrinterConfig
|
||||||
|
import haus.nightmare.lib3270j.printer.PrinterConstants
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ScriptAndPrinterTest {
|
||||||
|
|
||||||
|
private lateinit var screenBuffer: ScreenBuffer
|
||||||
|
private lateinit var translator: EbcdicTranslator
|
||||||
|
private lateinit var inputProcessor: InputProcessor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val model = TerminalModel.IBM_3279_2
|
||||||
|
translator = EbcdicTranslator()
|
||||||
|
screenBuffer = ScreenBuffer(model, translator)
|
||||||
|
inputProcessor = InputProcessor(screenBuffer, translator, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEmulateInputMnemonicTokens() {
|
||||||
|
// Position 0: Unprotected input field
|
||||||
|
screenBuffer.setCellFA(0, FA_PRINTABLE.toByte())
|
||||||
|
screenBuffer.cursorAddress = 1
|
||||||
|
|
||||||
|
inputProcessor.sendKeys("HELLO[enter]")
|
||||||
|
assertEquals('H', screenBuffer.getCell(1).ucs4)
|
||||||
|
assertEquals('E', screenBuffer.getCell(2).ucs4)
|
||||||
|
assertEquals('L', screenBuffer.getCell(3).ucs4)
|
||||||
|
assertEquals('L', screenBuffer.getCell(4).ucs4)
|
||||||
|
assertEquals('O', screenBuffer.getCell(5).ucs4)
|
||||||
|
assertEquals(AID_ENTER, inputProcessor.lastAid)
|
||||||
|
|
||||||
|
inputProcessor.reset()
|
||||||
|
inputProcessor.sendKeys("[pf3]")
|
||||||
|
assertEquals(AID_PF3, inputProcessor.lastAid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPrinterConfigParameters() {
|
||||||
|
val config = PrinterConfig("10.0.0.1", 992, "PRT01", true).apply {
|
||||||
|
associatedDisplayLuName = "DSP01"
|
||||||
|
codePage = "1047"
|
||||||
|
mpp = 132
|
||||||
|
mpl = 66
|
||||||
|
destinationType = PrinterConfig.DestinationType.MEMORY
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("10.0.0.1", config.host)
|
||||||
|
assertEquals(992, config.port)
|
||||||
|
assertEquals("PRT01", config.printerLuName)
|
||||||
|
assertEquals("DSP01", config.associatedDisplayLuName)
|
||||||
|
assertTrue(config.isUseTls)
|
||||||
|
assertEquals("1047", config.codePage)
|
||||||
|
assertEquals(132, config.mpp)
|
||||||
|
assertEquals(66, config.mpl)
|
||||||
|
assertEquals(PrinterConfig.DestinationType.MEMORY, config.destinationType)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user