IBM-DYNAMIC
This commit is contained in:
@@ -25,6 +25,7 @@ import android.content.Context
|
|||||||
import android.view.KeyCharacterMap
|
import android.view.KeyCharacterMap
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
import haus.nightmare.a3270.service.TerminalService
|
import haus.nightmare.a3270.service.TerminalService
|
||||||
|
import haus.nightmare.a3270.storage.AppSettings
|
||||||
import haus.nightmare.a3270.storage.HostStorage
|
import haus.nightmare.a3270.storage.HostStorage
|
||||||
import haus.nightmare.a3270.ui.ConnectDialog
|
import haus.nightmare.a3270.ui.ConnectDialog
|
||||||
import haus.nightmare.a3270.ui.FileTransferDialog
|
import haus.nightmare.a3270.ui.FileTransferDialog
|
||||||
@@ -51,19 +52,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
rootLogger.removeHandler(h)
|
rootLogger.removeHandler(h)
|
||||||
}
|
}
|
||||||
rootLogger.addHandler(object : java.util.logging.Handler() {
|
rootLogger.addHandler(object : java.util.logging.Handler() {
|
||||||
override fun publish(record: java.util.logging.LogRecord?) {
|
override fun publish(record: java.util.logging.LogRecord) {
|
||||||
if (record == null) return
|
val tag = "lib3270j-" + (record.loggerName?.substringAfterLast('.') ?: "core")
|
||||||
val msg = record.message ?: ""
|
val msg = record.message ?: ""
|
||||||
val tag = "a3270-" + (record.loggerName?.substringAfterLast('.') ?: "lib3270j")
|
when (record.level.intValue()) {
|
||||||
when {
|
java.util.logging.Level.SEVERE.intValue() -> android.util.Log.e(tag, msg, record.thrown)
|
||||||
record.level.intValue() >= java.util.logging.Level.SEVERE.intValue() ->
|
java.util.logging.Level.WARNING.intValue() -> android.util.Log.w(tag, msg, record.thrown)
|
||||||
android.util.Log.e(tag, msg, record.thrown)
|
java.util.logging.Level.INFO.intValue() -> android.util.Log.i(tag, msg, record.thrown)
|
||||||
record.level.intValue() >= java.util.logging.Level.WARNING.intValue() ->
|
java.util.logging.Level.CONFIG.intValue() -> android.util.Log.d(tag, msg, record.thrown)
|
||||||
android.util.Log.w(tag, msg, record.thrown)
|
else -> android.util.Log.v(tag, msg, record.thrown)
|
||||||
record.level.intValue() >= java.util.logging.Level.INFO.intValue() ->
|
|
||||||
android.util.Log.i(tag, msg, record.thrown)
|
|
||||||
else ->
|
|
||||||
android.util.Log.d(tag, msg, record.thrown)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun flush() {}
|
override fun flush() {}
|
||||||
@@ -74,6 +71,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private val viewModel: TerminalViewModel by viewModels()
|
private val viewModel: TerminalViewModel by viewModels()
|
||||||
private val isShiftPressedState = mutableStateOf(false)
|
private val isShiftPressedState = mutableStateOf(false)
|
||||||
|
private val isFindDialogOpenState = mutableStateOf(false)
|
||||||
|
private val isFtDialogOpenState = mutableStateOf(false)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -88,17 +87,29 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
MaterialTheme(
|
val uiTheme by viewModel.uiTheme.collectAsState()
|
||||||
colorScheme = darkColorScheme(
|
val colorScheme = if (uiTheme.equals("LIGHT", ignoreCase = true)) {
|
||||||
|
lightColorScheme(
|
||||||
|
background = Color(0xFFF2F2F4),
|
||||||
|
surface = Color.White,
|
||||||
|
primary = Color(0xFF1971C2)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
darkColorScheme(
|
||||||
background = Color.Black,
|
background = Color.Black,
|
||||||
surface = Color(0xFF1E1E1E),
|
surface = Color(0xFF1E1E1E),
|
||||||
primary = Color(0xFF339AF0)
|
primary = Color(0xFF339AF0)
|
||||||
)
|
)
|
||||||
) {
|
}
|
||||||
|
MaterialTheme(colorScheme = colorScheme) {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
isShiftPressed = isShiftPressedState.value,
|
isShiftPressed = isShiftPressedState.value,
|
||||||
onClearShift = { isShiftPressedState.value = false }
|
onClearShift = { isShiftPressedState.value = false },
|
||||||
|
isFindDialogOpen = isFindDialogOpenState.value,
|
||||||
|
onSetFindDialogOpen = { isFindDialogOpenState.value = it },
|
||||||
|
isFtDialogOpen = isFtDialogOpenState.value,
|
||||||
|
onSetFtDialogOpen = { isFtDialogOpenState.value = it }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,6 +210,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
// 2. Ctrl Shortcuts
|
// 2. Ctrl Shortcuts
|
||||||
if (ctrl) {
|
if (ctrl) {
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_F -> {
|
||||||
|
isFindDialogOpenState.value = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_D -> {
|
||||||
|
viewModel.disconnect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
KeyEvent.KEYCODE_V -> {
|
KeyEvent.KEYCODE_V -> {
|
||||||
pasteFromClipboard()
|
pasteFromClipboard()
|
||||||
return true
|
return true
|
||||||
@@ -246,9 +265,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Alt Shortcuts (PA keys, Clear, Reset)
|
// 3. Alt Shortcuts (PA keys, Clear, Reset, Enter, Erase, Attn, SysReq, CursorSelect, Ruler, FT)
|
||||||
if (alt) {
|
if (alt) {
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
||||||
|
viewModel.sendAid(AID_ENTER)
|
||||||
|
return true
|
||||||
|
}
|
||||||
KeyEvent.KEYCODE_1 -> {
|
KeyEvent.KEYCODE_1 -> {
|
||||||
viewModel.sendAid(AID_PA1)
|
viewModel.sendAid(AID_PA1)
|
||||||
return true
|
return true
|
||||||
@@ -261,7 +284,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
viewModel.sendAid(AID_PA3)
|
viewModel.sendAid(AID_PA3)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_C -> {
|
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_K -> {
|
||||||
viewModel.sendAid(AID_CLEAR)
|
viewModel.sendAid(AID_CLEAR)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -269,12 +292,34 @@ class MainActivity : ComponentActivity() {
|
|||||||
viewModel.resetKeyboard()
|
viewModel.resetKeyboard()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_D -> {
|
KeyEvent.KEYCODE_E -> {
|
||||||
viewModel.deleteWord()
|
viewModel.eraseInput()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_E -> {
|
KeyEvent.KEYCODE_A -> {
|
||||||
viewModel.fieldEnd()
|
viewModel.attn()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_S -> {
|
||||||
|
viewModel.sysReq()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_Q -> {
|
||||||
|
viewModel.cursorSelect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_X -> {
|
||||||
|
viewModel.toggleCrosshairRuler()
|
||||||
|
val isRuler = viewModel.isCrosshairRuler.value
|
||||||
|
Toast.makeText(this, "Crosshair Ruler: " + (if (isRuler) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_T -> {
|
||||||
|
isFtDialogOpenState.value = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_D -> {
|
||||||
|
viewModel.deleteWord()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_L -> {
|
KeyEvent.KEYCODE_L -> {
|
||||||
@@ -424,7 +469,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: TerminalViewModel,
|
viewModel: TerminalViewModel,
|
||||||
isShiftPressed: Boolean = false,
|
isShiftPressed: Boolean = false,
|
||||||
onClearShift: () -> Unit = {}
|
onClearShift: () -> Unit = {},
|
||||||
|
isFindDialogOpen: Boolean = false,
|
||||||
|
onSetFindDialogOpen: (Boolean) -> Unit = {},
|
||||||
|
isFtDialogOpen: Boolean = false,
|
||||||
|
onSetFtDialogOpen: (Boolean) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val connectionState by viewModel.connectionState.collectAsState()
|
val connectionState by viewModel.connectionState.collectAsState()
|
||||||
@@ -449,6 +498,28 @@ fun MainScreen(
|
|||||||
val ftState by viewModel.ftState.collectAsState()
|
val ftState by viewModel.ftState.collectAsState()
|
||||||
val isLightPenMode by viewModel.isLightPenMode.collectAsState()
|
val isLightPenMode by viewModel.isLightPenMode.collectAsState()
|
||||||
|
|
||||||
|
val isCrosshairRuler by viewModel.isCrosshairRuler.collectAsState()
|
||||||
|
val cursorStyle by viewModel.cursorStyle.collectAsState()
|
||||||
|
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
|
||||||
|
val uiTheme by viewModel.uiTheme.collectAsState()
|
||||||
|
val isInsertMode by viewModel.isInsertMode.collectAsState()
|
||||||
|
|
||||||
|
val isNumericField = remember(screenBuffer, cursorAddr, screenVersion) {
|
||||||
|
if (screenBuffer != null && screenBuffer?.isFormatted == true) {
|
||||||
|
val fa = screenBuffer?.getFieldAttributeAt(cursorAddr) ?: 0.toByte()
|
||||||
|
(fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
val modelName = remember(rows, cols) {
|
||||||
|
when {
|
||||||
|
rows == 24 && cols == 80 -> "3279-2"
|
||||||
|
rows == 32 && cols == 80 -> "3279-3"
|
||||||
|
rows == 43 && cols == 80 -> "3279-4"
|
||||||
|
rows == 27 && cols == 132 -> "3279-5"
|
||||||
|
else -> "DYNAMIC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var showConnectDialog by remember { mutableStateOf(false) }
|
var showConnectDialog by remember { mutableStateOf(false) }
|
||||||
var showFtDialog by remember { mutableStateOf(false) }
|
var showFtDialog by remember { mutableStateOf(false) }
|
||||||
var showSettingsDialog by remember { mutableStateOf(false) }
|
var showSettingsDialog by remember { mutableStateOf(false) }
|
||||||
@@ -458,6 +529,20 @@ fun MainScreen(
|
|||||||
var showScriptDialog by remember { mutableStateOf(false) }
|
var showScriptDialog by remember { mutableStateOf(false) }
|
||||||
var showPrinterSessionDialog by remember { mutableStateOf(false) }
|
var showPrinterSessionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(isFindDialogOpen) {
|
||||||
|
if (isFindDialogOpen) {
|
||||||
|
showFindDialog = true
|
||||||
|
onSetFindDialogOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isFtDialogOpen) {
|
||||||
|
if (isFtDialogOpen) {
|
||||||
|
showFtDialog = true
|
||||||
|
onSetFtDialogOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
||||||
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
@@ -476,12 +561,17 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
val startupBehavior = AppSettings.getStartupBehavior(context)
|
||||||
|
when (startupBehavior) {
|
||||||
|
AppSettings.StartupBehavior.AUTO_CONNECT -> {
|
||||||
val autoHost = HostStorage.getAutoConnectHost(context)
|
val autoHost = HostStorage.getAutoConnectHost(context)
|
||||||
if (autoHost != null && !connectionState.isConnected()) {
|
if (autoHost != null && !connectionState.isConnected()) {
|
||||||
viewModel.connect(
|
viewModel.connect(
|
||||||
autoHost.host,
|
autoHost.host,
|
||||||
autoHost.port,
|
autoHost.port,
|
||||||
autoHost.model,
|
autoHost.model,
|
||||||
|
autoHost.dynamicRows,
|
||||||
|
autoHost.dynamicCols,
|
||||||
autoHost.luName,
|
autoHost.luName,
|
||||||
autoHost.hostType,
|
autoHost.hostType,
|
||||||
autoHost.useTls,
|
autoHost.useTls,
|
||||||
@@ -497,6 +587,16 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppSettings.StartupBehavior.SHOW_CONNECT -> {
|
||||||
|
if (!connectionState.isConnected()) {
|
||||||
|
showConnectDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppSettings.StartupBehavior.DO_NOTHING -> {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(connectionState) {
|
LaunchedEffect(connectionState) {
|
||||||
if (connectionState.isConnected()) {
|
if (connectionState.isConnected()) {
|
||||||
@@ -532,6 +632,13 @@ fun MainScreen(
|
|||||||
blinkCursor = cursorBlink,
|
blinkCursor = cursorBlink,
|
||||||
searchHighlightAddr = searchHighlightAddr,
|
searchHighlightAddr = searchHighlightAddr,
|
||||||
searchHighlightLen = searchHighlightLen,
|
searchHighlightLen = searchHighlightLen,
|
||||||
|
crosshairRuler = isCrosshairRuler,
|
||||||
|
cursorStyle = cursorStyle,
|
||||||
|
isInsertMode = isInsertMode,
|
||||||
|
blockSelectMode = blockSelectMode,
|
||||||
|
onPasteLineWrap = { text, wrapCol, wordWrap ->
|
||||||
|
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
|
||||||
|
},
|
||||||
onTapAddress = { addr ->
|
onTapAddress = { addr ->
|
||||||
viewModel.setCursor(addr)
|
viewModel.setCursor(addr)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
@@ -664,9 +771,11 @@ fun MainScreen(
|
|||||||
graphicsMode = defaultGraphicsMode,
|
graphicsMode = defaultGraphicsMode,
|
||||||
codePage = codePage,
|
codePage = codePage,
|
||||||
isLightPenMode = isLightPenMode,
|
isLightPenMode = isLightPenMode,
|
||||||
isInsertMode = viewModel.getClient()?.inputProcessor?.isInsertMode ?: false,
|
isInsertMode = isInsertMode,
|
||||||
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
||||||
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: ""
|
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
||||||
|
isNumericField = isNumericField,
|
||||||
|
modelName = modelName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -674,9 +783,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, cp, pType, pHost, pPort, pUser, pPass ->
|
onConnect = { host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
|
||||||
showConnectDialog = false
|
showConnectDialog = false
|
||||||
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
|
viewModel.connect(host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -709,10 +818,24 @@ fun MainScreen(
|
|||||||
initialVerifyCerts = verifyCerts,
|
initialVerifyCerts = verifyCerts,
|
||||||
initialDefaultGraphicsMode = defaultGraphicsMode,
|
initialDefaultGraphicsMode = defaultGraphicsMode,
|
||||||
initialDefaultCodePage = codePage,
|
initialDefaultCodePage = codePage,
|
||||||
|
initialUiTheme = uiTheme,
|
||||||
|
initialCrosshairRuler = isCrosshairRuler,
|
||||||
|
initialCursorStyle = cursorStyle,
|
||||||
|
initialBlockSelectMode = blockSelectMode,
|
||||||
|
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
|
||||||
|
initialDynamicRows = AppSettings.getDynamicRows(context),
|
||||||
|
initialDynamicCols = AppSettings.getDynamicCols(context),
|
||||||
onDismiss = { showSettingsDialog = false },
|
onDismiss = { showSettingsDialog = false },
|
||||||
onSave = { mask, blink, haptic, verify, gfx, defaultCp ->
|
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, startup, dynRows, dynCols ->
|
||||||
showSettingsDialog = false
|
showSettingsDialog = false
|
||||||
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
||||||
|
viewModel.setUiTheme(theme)
|
||||||
|
viewModel.setCrosshairRuler(crosshair)
|
||||||
|
viewModel.setCursorStyle(style)
|
||||||
|
viewModel.setBlockSelectMode(blockSel)
|
||||||
|
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
|
||||||
|
AppSettings.setDynamicRows(context, dynRows)
|
||||||
|
AppSettings.setDynamicCols(context, dynCols)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -146,6 +146,21 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _isLightPenMode = MutableStateFlow(false)
|
private val _isLightPenMode = MutableStateFlow(false)
|
||||||
val isLightPenMode: StateFlow<Boolean> = _isLightPenMode.asStateFlow()
|
val isLightPenMode: StateFlow<Boolean> = _isLightPenMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _isCrosshairRuler = MutableStateFlow(AppSettings.isCrosshairRulerEnabled(application))
|
||||||
|
val isCrosshairRuler: StateFlow<Boolean> = _isCrosshairRuler.asStateFlow()
|
||||||
|
|
||||||
|
private val _cursorStyle = MutableStateFlow(AppSettings.getCursorStyle(application))
|
||||||
|
val cursorStyle: StateFlow<String> = _cursorStyle.asStateFlow()
|
||||||
|
|
||||||
|
private val _blockSelectMode = MutableStateFlow(AppSettings.isBlockSelectModeEnabled(application))
|
||||||
|
val blockSelectMode: StateFlow<Boolean> = _blockSelectMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application))
|
||||||
|
val uiTheme: StateFlow<String> = _uiTheme.asStateFlow()
|
||||||
|
|
||||||
|
private val _isInsertMode = MutableStateFlow(false)
|
||||||
|
val isInsertMode: StateFlow<Boolean> = _isInsertMode.asStateFlow()
|
||||||
|
|
||||||
fun toggleLightPen() {
|
fun toggleLightPen() {
|
||||||
_isLightPenMode.value = !_isLightPenMode.value
|
_isLightPenMode.value = !_isLightPenMode.value
|
||||||
}
|
}
|
||||||
@@ -154,6 +169,53 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_isLightPenMode.value = enabled
|
_isLightPenMode.value = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setCrosshairRuler(enabled: Boolean) {
|
||||||
|
AppSettings.setCrosshairRulerEnabled(getApplication(), enabled)
|
||||||
|
_isCrosshairRuler.value = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleCrosshairRuler() {
|
||||||
|
setCrosshairRuler(!_isCrosshairRuler.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCursorStyle(style: String) {
|
||||||
|
AppSettings.setCursorStyle(getApplication(), style)
|
||||||
|
_cursorStyle.value = style
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBlockSelectMode(enabled: Boolean) {
|
||||||
|
AppSettings.setBlockSelectModeEnabled(getApplication(), enabled)
|
||||||
|
_blockSelectMode.value = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiTheme(theme: String) {
|
||||||
|
AppSettings.setUiTheme(getApplication(), theme)
|
||||||
|
_uiTheme.value = theme
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadSettings() {
|
||||||
|
val app = getApplication<Application>()
|
||||||
|
_maskHiddenInput.value = AppSettings.isMaskHiddenInputEnabled(app)
|
||||||
|
_cursorBlink.value = AppSettings.isCursorBlinkEnabled(app)
|
||||||
|
_hapticFeedback.value = AppSettings.isHapticFeedbackEnabled(app)
|
||||||
|
_verifyCerts.value = AppSettings.isVerifyCertsEnabled(app)
|
||||||
|
_defaultGraphicsMode.value = AppSettings.getDefaultGraphicsMode(app)
|
||||||
|
_codePage.value = AppSettings.getDefaultCodePage(app)
|
||||||
|
_isCrosshairRuler.value = AppSettings.isCrosshairRulerEnabled(app)
|
||||||
|
_cursorStyle.value = AppSettings.getCursorStyle(app)
|
||||||
|
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
|
||||||
|
_uiTheme.value = AppSettings.getUiTheme(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pasteLineWrap(text: String, endCol: Int, wordWrap: Boolean) {
|
||||||
|
val cur = client
|
||||||
|
if (cur != null && cur.connectionState.isFullSession) {
|
||||||
|
val curPos = cur.screenBuffer.cursorAddress
|
||||||
|
cur.ps?.pasteLineWrap(text, curPos, endCol, wordWrap)
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setSearchHighlight(addr: Int, len: Int) {
|
fun setSearchHighlight(addr: Int, len: Int) {
|
||||||
_searchHighlightAddr.value = addr
|
_searchHighlightAddr.value = addr
|
||||||
_searchHighlightLen.value = len
|
_searchHighlightLen.value = len
|
||||||
@@ -352,6 +414,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
c.sendNVTChar('\u001B')
|
c.sendNVTChar('\u001B')
|
||||||
} else {
|
} else {
|
||||||
ip.reset()
|
ip.reset()
|
||||||
|
_isInsertMode.value = ip.isInsertMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.WordLeft -> {
|
is TerminalInputAction.WordLeft -> {
|
||||||
@@ -374,8 +437,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.DeleteWord -> {
|
is TerminalInputAction.DeleteWord -> {
|
||||||
if (!isNvt) {
|
if (isNvt) {
|
||||||
ip.isKeyboardLocked = false
|
c.sendNVTString("\u0017")
|
||||||
|
} else {
|
||||||
ip.processDeleteWord()
|
ip.processDeleteWord()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,13 +449,15 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.Attn -> {
|
is TerminalInputAction.Attn -> {
|
||||||
if (!isNvt) {
|
if (isNvt) {
|
||||||
ip.attn()
|
c.sendNVTChar('\u0003')
|
||||||
|
} else {
|
||||||
|
c.attn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.SysReq -> {
|
is TerminalInputAction.SysReq -> {
|
||||||
if (!isNvt) {
|
if (!isNvt) {
|
||||||
ip.sysReq()
|
c.sysReq()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.Dup -> {
|
is TerminalInputAction.Dup -> {
|
||||||
@@ -407,6 +473,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
is TerminalInputAction.ToggleInsert -> {
|
is TerminalInputAction.ToggleInsert -> {
|
||||||
if (!isNvt) {
|
if (!isNvt) {
|
||||||
ip.processToggleInsert()
|
ip.processToggleInsert()
|
||||||
|
_isInsertMode.value = ip.isInsertMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.SetCursor -> {
|
is TerminalInputAction.SetCursor -> {
|
||||||
@@ -543,6 +610,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
host: String,
|
host: String,
|
||||||
port: Int = 23,
|
port: Int = 23,
|
||||||
modelNum: Int = 2,
|
modelNum: Int = 2,
|
||||||
|
dynamicRows: Int = 62,
|
||||||
|
dynamicCols: Int = 160,
|
||||||
luName: String = "",
|
luName: String = "",
|
||||||
hostType: String = "TSO",
|
hostType: String = "TSO",
|
||||||
useTls: Boolean = false,
|
useTls: Boolean = false,
|
||||||
@@ -577,6 +646,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
|
|
||||||
val model = when (modelNum) {
|
val model = when (modelNum) {
|
||||||
|
0 -> TerminalModel.IBM_DYNAMIC
|
||||||
3 -> TerminalModel.IBM_3279_3
|
3 -> TerminalModel.IBM_3279_3
|
||||||
4 -> TerminalModel.IBM_3279_4
|
4 -> TerminalModel.IBM_3279_4
|
||||||
5 -> TerminalModel.IBM_3279_5
|
5 -> TerminalModel.IBM_3279_5
|
||||||
@@ -597,6 +667,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_oiaText.value = "Connecting to $effectiveHost:$effectivePort" + (if (effectiveTls) " [TLS]" else "") + "..."
|
_oiaText.value = "Connecting to $effectiveHost:$effectivePort" + (if (effectiveTls) " [TLS]" else "") + "..."
|
||||||
|
|
||||||
val config = ConnectionConfig(effectiveHost, effectivePort, model).apply {
|
val config = ConnectionConfig(effectiveHost, effectivePort, model).apply {
|
||||||
|
if (model == TerminalModel.IBM_DYNAMIC) {
|
||||||
|
setDynamicDimensions(dynamicRows, dynamicCols)
|
||||||
|
}
|
||||||
if (luName.isNotBlank()) {
|
if (luName.isNotBlank()) {
|
||||||
setLuName(luName)
|
setLuName(luName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ package haus.nightmare.a3270.storage
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.Reader
|
||||||
|
import java.io.Writer
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object AppSettings {
|
object AppSettings {
|
||||||
private const val PREFS_NAME = "a3270_settings"
|
private const val PREFS_NAME = "a3270_settings"
|
||||||
@@ -13,6 +20,21 @@ object AppSettings {
|
|||||||
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
||||||
private const val KEY_DEFAULT_CODE_PAGE = "default_code_page"
|
private const val KEY_DEFAULT_CODE_PAGE = "default_code_page"
|
||||||
|
|
||||||
|
// New settings aligned with j3270
|
||||||
|
private const val KEY_UI_THEME = "ui_theme"
|
||||||
|
private const val KEY_CROSSHAIR_RULER = "crosshair_ruler"
|
||||||
|
private const val KEY_CURSOR_STYLE = "cursor_style"
|
||||||
|
private const val KEY_BLOCK_SELECT_MODE = "block_select_mode"
|
||||||
|
private const val KEY_STARTUP_BEHAVIOR = "startup_behavior"
|
||||||
|
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
|
||||||
|
private const val KEY_DYNAMIC_COLS = "dynamic_cols"
|
||||||
|
|
||||||
|
enum class StartupBehavior {
|
||||||
|
SHOW_CONNECT,
|
||||||
|
DO_NOTHING,
|
||||||
|
AUTO_CONNECT
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -64,4 +86,161 @@ object AppSettings {
|
|||||||
fun setDefaultCodePage(context: Context, codePage: String) {
|
fun setDefaultCodePage(context: Context, codePage: String) {
|
||||||
getPrefs(context).edit().putString(KEY_DEFAULT_CODE_PAGE, codePage).apply()
|
getPrefs(context).edit().putString(KEY_DEFAULT_CODE_PAGE, codePage).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== UI Theme ==========
|
||||||
|
fun getUiTheme(context: Context): String {
|
||||||
|
return getPrefs(context).getString(KEY_UI_THEME, "DARK") ?: "DARK"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiTheme(context: Context, theme: String) {
|
||||||
|
getPrefs(context).edit().putString(KEY_UI_THEME, if (theme.equals("LIGHT", ignoreCase = true)) "LIGHT" else "DARK").apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Crosshair Ruler ==========
|
||||||
|
fun isCrosshairRulerEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_CROSSHAIR_RULER, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCrosshairRulerEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_CROSSHAIR_RULER, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Cursor Style ==========
|
||||||
|
fun getCursorStyle(context: Context): String {
|
||||||
|
return getPrefs(context).getString(KEY_CURSOR_STYLE, "BLOCK") ?: "BLOCK"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCursorStyle(context: Context, style: String) {
|
||||||
|
val s = if (style.equals("UNDERLINE", ignoreCase = true)) "UNDERLINE" else "BLOCK"
|
||||||
|
getPrefs(context).edit().putString(KEY_CURSOR_STYLE, s).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Block Select Mode ==========
|
||||||
|
fun isBlockSelectModeEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_BLOCK_SELECT_MODE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBlockSelectModeEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_BLOCK_SELECT_MODE, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Startup Behavior ==========
|
||||||
|
fun getStartupBehavior(context: Context): StartupBehavior {
|
||||||
|
val s = getPrefs(context).getString(KEY_STARTUP_BEHAVIOR, StartupBehavior.SHOW_CONNECT.name)
|
||||||
|
return try {
|
||||||
|
StartupBehavior.valueOf(s ?: StartupBehavior.SHOW_CONNECT.name)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
StartupBehavior.SHOW_CONNECT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStartupBehavior(context: Context, behavior: StartupBehavior) {
|
||||||
|
getPrefs(context).edit().putString(KEY_STARTUP_BEHAVIOR, behavior.name).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Dynamic Screen Dimensions ==========
|
||||||
|
fun getDynamicRows(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_DYNAMIC_ROWS, 62).coerceIn(24, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDynamicRows(context: Context, rows: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_DYNAMIC_ROWS, rows.coerceIn(24, 255)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDynamicCols(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_DYNAMIC_COLS, 160).coerceIn(80, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDynamicCols(context: Context, cols: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_DYNAMIC_COLS, cols.coerceIn(80, 255)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== INI Config Export / Import ==========
|
||||||
|
|
||||||
|
fun exportToIni(context: Context, writer: Writer) {
|
||||||
|
val p = PrintWriter(writer)
|
||||||
|
val timeStamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
||||||
|
p.println("; a3270 / j3270 Configuration File")
|
||||||
|
p.println("; Exported on $timeStamp")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
p.println("[appearance]")
|
||||||
|
p.println("javaUiTheme = ${getUiTheme(context)}")
|
||||||
|
p.println("crosshairRuler = ${isCrosshairRulerEnabled(context)}")
|
||||||
|
p.println("cursorStyle = ${getCursorStyle(context)}")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
p.println("[behavior]")
|
||||||
|
p.println("startupBehavior = ${getStartupBehavior(context).name}")
|
||||||
|
val autoHost = HostStorage.getAutoConnectHost(context)
|
||||||
|
if (autoHost != null) {
|
||||||
|
p.println("autoConnectHost = ${autoHost.host}")
|
||||||
|
p.println("autoConnectPort = ${autoHost.port}")
|
||||||
|
p.println("autoConnectTls = ${autoHost.useTls}")
|
||||||
|
p.println("autoConnectVerifyCert = ${autoHost.tlsVerifyCert}")
|
||||||
|
p.println("autoConnectTn3270e = ${autoHost.tn3270e}")
|
||||||
|
}
|
||||||
|
p.println("codePage = ${getDefaultCodePage(context)}")
|
||||||
|
p.println("dynamicRows = ${getDynamicRows(context)}")
|
||||||
|
p.println("dynamicCols = ${getDynamicCols(context)}")
|
||||||
|
p.println("blockSelectMode = ${isBlockSelectModeEnabled(context)}")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
p.println("[graphics]")
|
||||||
|
p.println("graphicsMode = ${getDefaultGraphicsMode(context)}")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
p.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFromIni(context: Context, reader: Reader) {
|
||||||
|
val bufferedReader = BufferedReader(reader)
|
||||||
|
var currentSection = ""
|
||||||
|
|
||||||
|
bufferedReader.forEachLine { rawLine ->
|
||||||
|
val line = rawLine.trim()
|
||||||
|
if (line.isNotEmpty() && !line.startsWith(";") && !line.startsWith("#")) {
|
||||||
|
if (line.startsWith("[") && line.endsWith("]")) {
|
||||||
|
currentSection = line.substring(1, line.length - 1).lowercase(Locale.US).trim()
|
||||||
|
} else {
|
||||||
|
val eq = line.indexOf('=')
|
||||||
|
if (eq >= 0) {
|
||||||
|
val key = line.substring(0, eq).trim()
|
||||||
|
val value = line.substring(eq + 1).trim()
|
||||||
|
applyConfigEntry(context, currentSection, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyConfigEntry(context: Context, section: String, key: String, value: String) {
|
||||||
|
when (section) {
|
||||||
|
"appearance" -> {
|
||||||
|
when (key.lowercase(Locale.US)) {
|
||||||
|
"javauitheme", "theme", "uitheme" -> setUiTheme(context, value)
|
||||||
|
"crosshairruler", "ruler" -> setCrosshairRulerEnabled(context, value.toBoolean())
|
||||||
|
"cursorstyle" -> setCursorStyle(context, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"behavior", "connection" -> {
|
||||||
|
when (key.lowercase(Locale.US)) {
|
||||||
|
"startupbehavior" -> {
|
||||||
|
try {
|
||||||
|
setStartupBehavior(context, StartupBehavior.valueOf(value.uppercase(Locale.US)))
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
"codepage", "charset" -> setDefaultCodePage(context, value)
|
||||||
|
"dynamicrows", "dynamic_rows" -> value.toIntOrNull()?.let { setDynamicRows(context, it) }
|
||||||
|
"dynamiccols", "dynamic_cols" -> value.toIntOrNull()?.let { setDynamicCols(context, it) }
|
||||||
|
"blockselectmode" -> setBlockSelectModeEnabled(context, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"graphics" -> {
|
||||||
|
if (key.equals("graphicsmode", ignoreCase = true) || key.equals("mode", ignoreCase = true)) {
|
||||||
|
setDefaultGraphicsMode(context, value.uppercase(Locale.US))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ data class SavedHost(
|
|||||||
val host: String,
|
val host: String,
|
||||||
val port: Int = 23,
|
val port: Int = 23,
|
||||||
val model: Int = 2,
|
val model: Int = 2,
|
||||||
|
val dynamicRows: Int = 62,
|
||||||
|
val dynamicCols: Int = 160,
|
||||||
val luName: String = "",
|
val luName: String = "",
|
||||||
val autoConnect: Boolean = false,
|
val autoConnect: Boolean = false,
|
||||||
val hostType: String = "TSO",
|
val hostType: String = "TSO",
|
||||||
@@ -33,6 +35,8 @@ data class SavedHost(
|
|||||||
put("host", host)
|
put("host", host)
|
||||||
put("port", port)
|
put("port", port)
|
||||||
put("model", model)
|
put("model", model)
|
||||||
|
put("dynamicRows", dynamicRows)
|
||||||
|
put("dynamicCols", dynamicCols)
|
||||||
put("luName", luName)
|
put("luName", luName)
|
||||||
put("autoConnect", autoConnect)
|
put("autoConnect", autoConnect)
|
||||||
put("hostType", hostType)
|
put("hostType", hostType)
|
||||||
@@ -57,6 +61,8 @@ data class SavedHost(
|
|||||||
host = json.optString("host", "127.0.0.1"),
|
host = json.optString("host", "127.0.0.1"),
|
||||||
port = json.optInt("port", 23),
|
port = json.optInt("port", 23),
|
||||||
model = json.optInt("model", 2),
|
model = json.optInt("model", 2),
|
||||||
|
dynamicRows = json.optInt("dynamicRows", 62),
|
||||||
|
dynamicCols = json.optInt("dynamicCols", 160),
|
||||||
luName = json.optString("luName", ""),
|
luName = json.optString("luName", ""),
|
||||||
autoConnect = json.optBoolean("autoConnect", false),
|
autoConnect = json.optBoolean("autoConnect", false),
|
||||||
hostType = json.optString("hostType", "TSO"),
|
hostType = json.optString("hostType", "TSO"),
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ fun ConnectDialog(
|
|||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
model: Int,
|
model: Int,
|
||||||
|
dynamicRows: Int,
|
||||||
|
dynamicCols: Int,
|
||||||
luName: String,
|
luName: String,
|
||||||
hostType: String,
|
hostType: String,
|
||||||
useTls: Boolean,
|
useTls: Boolean,
|
||||||
@@ -61,9 +63,11 @@ fun ConnectDialog(
|
|||||||
var selectedHostId by remember { mutableStateOf<String?>(null) }
|
var selectedHostId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
var profileName by remember { mutableStateOf("") }
|
var profileName by remember { mutableStateOf("") }
|
||||||
var host by remember { mutableStateOf("127.0.0.1") }
|
var host by remember { mutableStateOf("") }
|
||||||
var portStr by remember { mutableStateOf("23") }
|
var portStr by remember { mutableStateOf("") }
|
||||||
var modelNum by remember { mutableIntStateOf(2) }
|
var modelNum by remember { mutableIntStateOf(2) }
|
||||||
|
var dynamicRowsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()) }
|
||||||
|
var dynamicColsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()) }
|
||||||
var luName by remember { mutableStateOf("") }
|
var luName by remember { mutableStateOf("") }
|
||||||
var autoConnect by remember { mutableStateOf(false) }
|
var autoConnect by remember { mutableStateOf(false) }
|
||||||
var hostType by remember { mutableStateOf("TSO") }
|
var hostType by remember { mutableStateOf("TSO") }
|
||||||
@@ -85,6 +89,8 @@ fun ConnectDialog(
|
|||||||
host = saved.host
|
host = saved.host
|
||||||
portStr = saved.port.toString()
|
portStr = saved.port.toString()
|
||||||
modelNum = saved.model
|
modelNum = saved.model
|
||||||
|
dynamicRowsStr = saved.dynamicRows.toString()
|
||||||
|
dynamicColsStr = saved.dynamicCols.toString()
|
||||||
luName = saved.luName
|
luName = saved.luName
|
||||||
autoConnect = saved.autoConnect
|
autoConnect = saved.autoConnect
|
||||||
hostType = saved.hostType
|
hostType = saved.hostType
|
||||||
@@ -104,9 +110,11 @@ fun ConnectDialog(
|
|||||||
fun clearFields() {
|
fun clearFields() {
|
||||||
selectedHostId = null
|
selectedHostId = null
|
||||||
profileName = ""
|
profileName = ""
|
||||||
host = "127.0.0.1"
|
host = ""
|
||||||
portStr = "23"
|
portStr = ""
|
||||||
modelNum = 2
|
modelNum = 2
|
||||||
|
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
|
||||||
|
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
||||||
luName = ""
|
luName = ""
|
||||||
autoConnect = false
|
autoConnect = false
|
||||||
hostType = "TSO"
|
hostType = "TSO"
|
||||||
@@ -126,14 +134,21 @@ fun ConnectDialog(
|
|||||||
fun saveCurrentProfile(): SavedHost? {
|
fun saveCurrentProfile(): SavedHost? {
|
||||||
val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23
|
val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23
|
||||||
val trimmedHost = host.trim()
|
val trimmedHost = host.trim()
|
||||||
if (trimmedHost.isBlank()) return null
|
if (trimmedHost.isBlank()) {
|
||||||
|
android.widget.Toast.makeText(context, "Please enter a Host / IP Address", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
return null
|
||||||
|
}
|
||||||
val nameToSave = profileName.trim().ifBlank { "$trimmedHost:$port" }
|
val nameToSave = profileName.trim().ifBlank { "$trimmedHost:$port" }
|
||||||
|
val dynRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
||||||
|
val dynCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
|
||||||
val hostToSave = SavedHost(
|
val hostToSave = SavedHost(
|
||||||
id = selectedHostId ?: java.util.UUID.randomUUID().toString(),
|
id = selectedHostId ?: java.util.UUID.randomUUID().toString(),
|
||||||
name = nameToSave,
|
name = nameToSave,
|
||||||
host = trimmedHost,
|
host = trimmedHost,
|
||||||
port = port,
|
port = port,
|
||||||
model = modelNum,
|
model = modelNum,
|
||||||
|
dynamicRows = dynRows,
|
||||||
|
dynamicCols = dynCols,
|
||||||
luName = luName.trim(),
|
luName = luName.trim(),
|
||||||
autoConnect = autoConnect,
|
autoConnect = autoConnect,
|
||||||
hostType = hostType,
|
hostType = hostType,
|
||||||
@@ -258,7 +273,7 @@ fun ConnectDialog(
|
|||||||
color = Color.White
|
color = Color.White
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "${saved.host}:${saved.port} (M${saved.model} - ${saved.hostType})$tlsTag$eTag$gfxTag" +
|
text = "${saved.host}:${saved.port} (${if (saved.model == 0) "Dyn ${saved.dynamicRows}x${saved.dynamicCols}" else "M" + saved.model} - ${saved.hostType})$tlsTag$eTag$gfxTag" +
|
||||||
if (saved.autoConnect) " [Auto]" else "",
|
if (saved.autoConnect) " [Auto]" else "",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = Color.LightGray
|
color = Color.LightGray
|
||||||
@@ -273,6 +288,8 @@ fun ConnectDialog(
|
|||||||
hostToConn.host,
|
hostToConn.host,
|
||||||
hostToConn.port,
|
hostToConn.port,
|
||||||
hostToConn.model,
|
hostToConn.model,
|
||||||
|
hostToConn.dynamicRows,
|
||||||
|
hostToConn.dynamicCols,
|
||||||
hostToConn.luName,
|
hostToConn.luName,
|
||||||
hostToConn.hostType,
|
hostToConn.hostType,
|
||||||
hostToConn.useTls,
|
hostToConn.useTls,
|
||||||
@@ -335,6 +352,7 @@ fun ConnectDialog(
|
|||||||
value = host,
|
value = host,
|
||||||
onValueChange = { host = it.filterNot { c -> c.isWhitespace() } },
|
onValueChange = { host = it.filterNot { c -> c.isWhitespace() } },
|
||||||
label = { Text("Host / IP Address") },
|
label = { Text("Host / IP Address") },
|
||||||
|
placeholder = { Text("127.0.0.1", color = Color.Gray) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -345,6 +363,7 @@ fun ConnectDialog(
|
|||||||
value = portStr,
|
value = portStr,
|
||||||
onValueChange = { portStr = it.filter { c -> c.isDigit() } },
|
onValueChange = { portStr = it.filter { c -> c.isDigit() } },
|
||||||
label = { Text("Port") },
|
label = { Text("Port") },
|
||||||
|
placeholder = { Text(if (useTls) "992" else "23", color = Color.Gray) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
modifier = Modifier.weight(0.32f)
|
modifier = Modifier.weight(0.32f)
|
||||||
@@ -363,7 +382,8 @@ fun ConnectDialog(
|
|||||||
2 to "M2",
|
2 to "M2",
|
||||||
3 to "M3",
|
3 to "M3",
|
||||||
4 to "M4",
|
4 to "M4",
|
||||||
5 to "M5"
|
5 to "M5",
|
||||||
|
0 to "DYN"
|
||||||
).forEach { (m, label) ->
|
).forEach { (m, label) ->
|
||||||
val isSelected = (modelNum == m)
|
val isSelected = (modelNum == m)
|
||||||
Surface(
|
Surface(
|
||||||
@@ -389,6 +409,32 @@ fun ConnectDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Dynamic Model selected, allow specifying Rows and Columns
|
||||||
|
if (modelNum == 0) {
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = dynamicRowsStr,
|
||||||
|
onValueChange = { dynamicRowsStr = it },
|
||||||
|
label = { Text("Dyn Rows (24-255)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = dynamicColsStr,
|
||||||
|
onValueChange = { dynamicColsStr = it },
|
||||||
|
label = { Text("Dyn Cols (80-255)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host System Type (TSO, CMS, CICS)
|
// Host System Type (TSO, CMS, CICS)
|
||||||
@@ -745,6 +791,8 @@ fun ConnectDialog(
|
|||||||
saved.host,
|
saved.host,
|
||||||
saved.port,
|
saved.port,
|
||||||
saved.model,
|
saved.model,
|
||||||
|
saved.dynamicRows,
|
||||||
|
saved.dynamicCols,
|
||||||
saved.luName,
|
saved.luName,
|
||||||
saved.hostType,
|
saved.hostType,
|
||||||
saved.useTls,
|
saved.useTls,
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ fun OiaStatusBar(
|
|||||||
isInsertMode: Boolean = false,
|
isInsertMode: Boolean = false,
|
||||||
inhibitReason: Int = 0,
|
inhibitReason: Int = 0,
|
||||||
luName: String = "",
|
luName: String = "",
|
||||||
|
isNumericField: Boolean = false,
|
||||||
|
modelName: String = "",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
|
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
|
||||||
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
||||||
val posStr = String.format("%02d/%02d", row, col)
|
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
|
||||||
|
|
||||||
val hostText = if (currentHost.isNotBlank()) currentHost else oiaText
|
val hostText = if (currentHost.isNotBlank()) currentHost else oiaText
|
||||||
|
|
||||||
@@ -63,13 +65,18 @@ fun OiaStatusBar(
|
|||||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> "X OP"
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> "X OP"
|
||||||
else -> "X LOCKED"
|
else -> "X LOCKED"
|
||||||
}
|
}
|
||||||
txt to Color(0xFFFF6B6B)
|
val clr = when (inhibitReason) {
|
||||||
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> Color(0xFFFF0000)
|
||||||
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> Color(0xFFFFFF00)
|
||||||
|
else -> Color(0xFFFFFFFF)
|
||||||
|
}
|
||||||
|
txt to clr
|
||||||
} else if (isKeyboardLocked) {
|
} else if (isKeyboardLocked) {
|
||||||
"X SYSTEM" to Color(0xFFFF6B6B)
|
"X SYSTEM" to Color(0xFFFFFFFF)
|
||||||
} else if (isInsertMode) {
|
} else if (isInsertMode) {
|
||||||
"INSERT" to Color(0xFF339AF0)
|
"INSERT" to Color(0xFF7890F0)
|
||||||
} else {
|
} else {
|
||||||
"READY" to Color(0xFF51CF66)
|
"READY" to Color(0xFF7890F0)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -89,7 +96,7 @@ fun OiaStatusBar(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(8.dp)
|
.size(8.dp)
|
||||||
.background(if (connectionState.isConnected()) Color(0xFF51CF66) else Color(0xFF868E96))
|
.background(if (connectionState.isConnected()) Color(0xFF7890F0) else Color(0xFF868E96))
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -159,6 +166,42 @@ fun OiaStatusBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field Status: NUM vs ALPHA
|
||||||
|
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Surface(
|
||||||
|
color = Color(0xFF339AF0).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isNumericField) "NUM" else "ALPHA",
|
||||||
|
color = if (isNumericField) Color(0xFFFFD43B) else Color(0xFF74C0FC),
|
||||||
|
fontSize = 9.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model / Dimensions Badge
|
||||||
|
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Surface(
|
||||||
|
color = Color(0xFF495057).copy(alpha = 0.3f),
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (modelName.isNotBlank()) modelName else "${rows}x${cols}",
|
||||||
|
color = Color(0xFFCED4DA),
|
||||||
|
fontSize = 9.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Light Pen Badge
|
// Light Pen Badge
|
||||||
if (isLightPenMode) {
|
if (isLightPenMode) {
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
@@ -230,11 +273,11 @@ fun OiaStatusBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor Position
|
// Cursor Position & Buffer Address
|
||||||
Text(
|
Text(
|
||||||
text = "R$posStr",
|
text = posStr,
|
||||||
color = Color(0xFF22B8CF),
|
color = Color(0xFF22B8CF),
|
||||||
fontSize = 11.sp,
|
fontSize = 10.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ import java.util.Locale
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PrinterSessionDialog(
|
fun PrinterSessionDialog(
|
||||||
initialHost: String = "127.0.0.1",
|
initialHost: String = "",
|
||||||
initialPort: Int = 23,
|
initialPort: Int = 0,
|
||||||
initialCodePage: String = "037",
|
initialCodePage: String = "037",
|
||||||
initialUseTls: Boolean = false,
|
initialUseTls: Boolean = false,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
@@ -50,8 +50,8 @@ fun PrinterSessionDialog(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
var host by remember { mutableStateOf(initialHost.ifBlank { "127.0.0.1" }) }
|
var host by remember { mutableStateOf(initialHost) }
|
||||||
var portStr by remember { mutableStateOf(if (initialPort > 0) initialPort.toString() else "23") }
|
var portStr by remember { mutableStateOf(if (initialPort > 0) initialPort.toString() else "") }
|
||||||
var printerLu by remember { mutableStateOf("") }
|
var printerLu by remember { mutableStateOf("") }
|
||||||
var assocDisplayLu by remember { mutableStateOf("") }
|
var assocDisplayLu by remember { mutableStateOf("") }
|
||||||
var codePage by remember { mutableStateOf(initialCodePage) }
|
var codePage by remember { mutableStateOf(initialCodePage) }
|
||||||
@@ -69,8 +69,9 @@ fun PrinterSessionDialog(
|
|||||||
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
|
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
|
||||||
|
|
||||||
fun startSession() {
|
fun startSession() {
|
||||||
val port = portStr.trim().toIntOrNull() ?: 23
|
val targetHost = host.trim().ifBlank { "127.0.0.1" }
|
||||||
val config = PrinterConfig(host.trim(), port).apply {
|
val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23
|
||||||
|
val config = PrinterConfig(targetHost, port).apply {
|
||||||
this.isUseTls = useTls
|
this.isUseTls = useTls
|
||||||
this.codePage = codePage
|
this.codePage = codePage
|
||||||
if (printerLu.isNotBlank()) this.printerLuName = printerLu.trim()
|
if (printerLu.isNotBlank()) this.printerLuName = printerLu.trim()
|
||||||
@@ -273,6 +274,7 @@ fun PrinterSessionDialog(
|
|||||||
value = host,
|
value = host,
|
||||||
onValueChange = { host = it },
|
onValueChange = { host = it },
|
||||||
label = { Text("Host") },
|
label = { Text("Host") },
|
||||||
|
placeholder = { Text("127.0.0.1", color = Color.Gray) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.weight(2.5f)
|
modifier = Modifier.weight(2.5f)
|
||||||
)
|
)
|
||||||
@@ -280,6 +282,7 @@ fun PrinterSessionDialog(
|
|||||||
value = portStr,
|
value = portStr,
|
||||||
onValueChange = { portStr = it },
|
onValueChange = { portStr = it },
|
||||||
label = { Text("Port") },
|
label = { Text("Port") },
|
||||||
|
placeholder = { Text(if (useTls) "992" else "23", color = Color.Gray) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
|
|||||||
@@ -316,20 +316,24 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
|
|||||||
sb.append("<!DOCTYPE html>\n<html>\n<head>\n")
|
sb.append("<!DOCTYPE html>\n<html>\n<head>\n")
|
||||||
sb.append("<meta charset=\"UTF-8\">\n")
|
sb.append("<meta charset=\"UTF-8\">\n")
|
||||||
sb.append("<style>\n")
|
sb.append("<style>\n")
|
||||||
sb.append("body { background-color: #0A0A0A; color: #00FF66; font-family: monospace; font-size: 14px; margin: 20px; }\n")
|
sb.append("body { background-color: #0A0A0A; color: #00FF00; font-family: monospace; font-size: 14px; margin: 20px; }\n")
|
||||||
sb.append("pre { line-height: 1.2; font-family: monospace; }\n")
|
sb.append("pre { line-height: 1.2; font-family: monospace; }\n")
|
||||||
sb.append(".c-green { color: #00FF66; }\n")
|
sb.append(".c-green { color: #00FF00; }\n")
|
||||||
sb.append(".c-white { color: #FFFFFF; }\n")
|
sb.append(".c-white { color: #FFFFFF; }\n")
|
||||||
sb.append(".c-blue { color: #3399FF; }\n")
|
sb.append(".c-blue { color: #7890F0; }\n")
|
||||||
sb.append(".c-red { color: #FF3333; }\n")
|
sb.append(".c-red { color: #FF0000; }\n")
|
||||||
sb.append(".c-yellow { color: #FFFF33; }\n")
|
sb.append(".c-yellow { color: #FFFF00; }\n")
|
||||||
sb.append(".c-turquoise { color: #00E5FF; }\n")
|
sb.append(".c-turquoise { color: #00FFFF; }\n")
|
||||||
sb.append(".c-pink { color: #FF66CC; }\n")
|
sb.append(".c-pink { color: #FF00FF; }\n")
|
||||||
|
sb.append(".c-orange { color: #FFA200; }\n")
|
||||||
|
sb.append(".c-mustard { color: #A0A000; }\n")
|
||||||
|
sb.append(".c-grey { color: #C0C0C0; }\n")
|
||||||
sb.append("</style>\n</head>\n<body>\n<pre>\n")
|
sb.append("</style>\n</head>\n<body>\n<pre>\n")
|
||||||
|
|
||||||
var currentHidden = false
|
var currentHidden = false
|
||||||
var currentProt = false
|
var currentProt = false
|
||||||
var currentHi = false
|
var currentHi = false
|
||||||
|
var currentFieldEa: haus.nightmare.lib3270j.screen.ExtendedAttribute? = null
|
||||||
|
|
||||||
for (r in 0 until rows) {
|
for (r in 0 until rows) {
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until cols) {
|
||||||
@@ -340,17 +344,41 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
|
|||||||
currentProt = (faVal and FA_PROTECT) != 0
|
currentProt = (faVal and FA_PROTECT) != 0
|
||||||
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
|
currentFieldEa = cell
|
||||||
sb.append(' ')
|
sb.append(' ')
|
||||||
} else {
|
} else {
|
||||||
if (currentHidden && maskHidden) {
|
if (currentHidden && maskHidden) {
|
||||||
sb.append(' ')
|
sb.append(' ')
|
||||||
} else {
|
} else {
|
||||||
val ch = cell.ucs4
|
val ch = cell.ucs4
|
||||||
val colorClass = when {
|
val fgByte = if (cell.fg != 0.toByte()) (cell.fg.toInt() and 0xFF)
|
||||||
currentHi -> "c-white"
|
else (currentFieldEa?.fg?.toInt()?.and(0xFF) ?: 0)
|
||||||
currentProt -> "c-blue"
|
val colorClass = if (fgByte in 0xf0..0xff) {
|
||||||
|
when (fgByte - 0xf0) {
|
||||||
|
1 -> "c-blue"
|
||||||
|
2 -> "c-red"
|
||||||
|
3 -> "c-pink"
|
||||||
|
4 -> "c-green"
|
||||||
|
5 -> "c-turquoise"
|
||||||
|
6 -> "c-yellow"
|
||||||
|
7 -> "c-white"
|
||||||
|
9 -> "c-blue"
|
||||||
|
10 -> "c-orange"
|
||||||
|
11 -> "c-pink"
|
||||||
|
12 -> "c-green"
|
||||||
|
13 -> "c-turquoise"
|
||||||
|
14 -> "c-mustard"
|
||||||
|
15 -> "c-grey"
|
||||||
else -> "c-green"
|
else -> "c-green"
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
when {
|
||||||
|
currentProt && currentHi -> "c-white"
|
||||||
|
currentProt && !currentHi -> "c-turquoise"
|
||||||
|
!currentProt && currentHi -> "c-red"
|
||||||
|
else -> "c-green"
|
||||||
|
}
|
||||||
|
}
|
||||||
val escChar = when (ch) {
|
val escChar = when (ch) {
|
||||||
'<' -> "<"
|
'<' -> "<"
|
||||||
'>' -> ">"
|
'>' -> ">"
|
||||||
@@ -394,6 +422,18 @@ private fun generateBitmap(
|
|||||||
|
|
||||||
if (screenBuffer == null) return bitmap
|
if (screenBuffer == null) return bitmap
|
||||||
|
|
||||||
|
// Layer 1: Vector graphics plane (under text)
|
||||||
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
|
val rgb = graphicsPlane.rgbBuffer
|
||||||
|
val gWidth = graphicsPlane.canvasWidth
|
||||||
|
val gHeight = graphicsPlane.canvasHeight
|
||||||
|
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||||
|
val gfxBmp = Bitmap.createBitmap(rgb, gWidth, gHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val scaledGfx = Bitmap.createScaledBitmap(gfxBmp, width, height, true)
|
||||||
|
canvas.drawBitmap(scaledGfx, 0f, 0f, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val textPaint = Paint().apply {
|
val textPaint = Paint().apply {
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
typeface = Typeface.MONOSPACE
|
typeface = Typeface.MONOSPACE
|
||||||
@@ -403,6 +443,7 @@ private fun generateBitmap(
|
|||||||
var currentHidden = false
|
var currentHidden = false
|
||||||
var currentProt = false
|
var currentProt = false
|
||||||
var currentHi = false
|
var currentHi = false
|
||||||
|
var currentFieldEa: haus.nightmare.lib3270j.screen.ExtendedAttribute? = null
|
||||||
|
|
||||||
for (r in 0 until rows) {
|
for (r in 0 until rows) {
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until cols) {
|
||||||
@@ -416,22 +457,47 @@ private fun generateBitmap(
|
|||||||
currentProt = (faVal and FA_PROTECT) != 0
|
currentProt = (faVal and FA_PROTECT) != 0
|
||||||
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
|
currentFieldEa = cell
|
||||||
} else {
|
} else {
|
||||||
val csVal = cell.cs.toInt() and 0xFF
|
val csVal = cell.cs.toInt() and 0xFF
|
||||||
val ecVal = cell.ec.toInt() and 0xFF
|
val ecVal = cell.ec.toInt() and 0xFF
|
||||||
|
val fgByte = if (cell.fg != 0.toByte()) (cell.fg.toInt() and 0xFF)
|
||||||
|
else (currentFieldEa?.fg?.toInt()?.and(0xFF) ?: 0)
|
||||||
|
val cellColor = if (fgByte in 0xf0..0xff) {
|
||||||
|
when (fgByte - 0xf0) {
|
||||||
|
1 -> android.graphics.Color.parseColor("#7890F0")
|
||||||
|
2 -> android.graphics.Color.parseColor("#FF0000")
|
||||||
|
3 -> android.graphics.Color.parseColor("#FF00FF")
|
||||||
|
4 -> android.graphics.Color.parseColor("#00FF00")
|
||||||
|
5 -> android.graphics.Color.parseColor("#00FFFF")
|
||||||
|
6 -> android.graphics.Color.parseColor("#FFFF00")
|
||||||
|
7 -> android.graphics.Color.WHITE
|
||||||
|
9 -> android.graphics.Color.parseColor("#000080")
|
||||||
|
10 -> android.graphics.Color.parseColor("#FFA200")
|
||||||
|
11 -> android.graphics.Color.parseColor("#800080")
|
||||||
|
12 -> android.graphics.Color.parseColor("#008000")
|
||||||
|
13 -> android.graphics.Color.parseColor("#008080")
|
||||||
|
14 -> android.graphics.Color.parseColor("#A0A000")
|
||||||
|
15 -> android.graphics.Color.parseColor("#C0C0C0")
|
||||||
|
else -> android.graphics.Color.parseColor("#00FF00")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when {
|
||||||
|
currentProt && currentHi -> android.graphics.Color.WHITE
|
||||||
|
currentProt && !currentHi -> android.graphics.Color.parseColor("#00FFFF")
|
||||||
|
!currentProt && currentHi -> android.graphics.Color.parseColor("#FF0000")
|
||||||
|
else -> android.graphics.Color.parseColor("#00FF00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var drawnAsPs = false
|
var drawnAsPs = false
|
||||||
if (csVal >= 0x40 && programSymbolManager != null) {
|
if (csVal >= 0x40 && programSymbolManager != null) {
|
||||||
val slot = programSymbolManager.getSymbol(csVal, ecVal)
|
val slot = programSymbolManager.getSymbol(csVal, ecVal)
|
||||||
if (slot != null) {
|
if (slot != null) {
|
||||||
val symWidth = slot.width
|
val symWidth = slot.width
|
||||||
val symHeight = slot.height
|
val symHeight = slot.height
|
||||||
val fgArgb = when {
|
val bgArgb = android.graphics.Color.TRANSPARENT
|
||||||
currentHi -> android.graphics.Color.WHITE
|
val rgbArray = slot.getRgbPixels(cellColor, bgArgb)
|
||||||
currentProt -> android.graphics.Color.parseColor("#3399FF")
|
|
||||||
else -> android.graphics.Color.parseColor("#00FF66")
|
|
||||||
}
|
|
||||||
val bgArgb = android.graphics.Color.parseColor("#0A0A0A")
|
|
||||||
val rgbArray = slot.getRgbPixels(fgArgb, bgArgb)
|
|
||||||
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
||||||
val symBmp = Bitmap.createBitmap(rgbArray, symWidth, symHeight, Bitmap.Config.ARGB_8888)
|
val symBmp = Bitmap.createBitmap(rgbArray, symWidth, symHeight, Bitmap.Config.ARGB_8888)
|
||||||
canvas.drawBitmap(symBmp, null, android.graphics.RectF(left, top, left + charWidth, top + charHeight), null)
|
canvas.drawBitmap(symBmp, null, android.graphics.RectF(left, top, left + charWidth, top + charHeight), null)
|
||||||
@@ -443,11 +509,7 @@ private fun generateBitmap(
|
|||||||
if (!drawnAsPs && (!currentHidden || !maskHidden)) {
|
if (!drawnAsPs && (!currentHidden || !maskHidden)) {
|
||||||
val ch = cell.ucs4
|
val ch = cell.ucs4
|
||||||
if (ch in ' '..'~' || ch > '\u007F') {
|
if (ch in ' '..'~' || ch > '\u007F') {
|
||||||
textPaint.color = when {
|
textPaint.color = cellColor
|
||||||
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)
|
canvas.drawText(ch.toString(), left + 2f, top + 18f, textPaint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,17 +517,5 @@ private fun generateBitmap(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overlay vector graphics plane if present
|
|
||||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
|
||||||
val rgb = graphicsPlane.rgbBuffer
|
|
||||||
val gWidth = graphicsPlane.canvasWidth
|
|
||||||
val gHeight = graphicsPlane.canvasHeight
|
|
||||||
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
|
||||||
val gfxBmp = Bitmap.createBitmap(rgb, gWidth, gHeight, Bitmap.Config.ARGB_8888)
|
|
||||||
val scaledGfx = Bitmap.createScaledBitmap(gfxBmp, width, height, true)
|
|
||||||
canvas.drawBitmap(scaledGfx, 0f, 0f, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,33 @@ fun SettingsDialog(
|
|||||||
initialVerifyCerts: Boolean = true,
|
initialVerifyCerts: Boolean = true,
|
||||||
initialDefaultGraphicsMode: String = "BOTH",
|
initialDefaultGraphicsMode: String = "BOTH",
|
||||||
initialDefaultCodePage: String = "037",
|
initialDefaultCodePage: String = "037",
|
||||||
|
initialUiTheme: String = "DARK",
|
||||||
|
initialCrosshairRuler: Boolean = false,
|
||||||
|
initialCursorStyle: String = "BLOCK",
|
||||||
|
initialBlockSelectMode: Boolean = false,
|
||||||
|
initialStartupBehavior: String = "SHOW_CONNECT",
|
||||||
|
initialDynamicRows: Int = 62,
|
||||||
|
initialDynamicCols: Int = 160,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean, verifyCerts: Boolean, defaultGraphicsMode: String, defaultCodePage: String) -> Unit
|
onSave: (
|
||||||
|
maskHiddenInput: Boolean,
|
||||||
|
cursorBlink: Boolean,
|
||||||
|
hapticFeedback: Boolean,
|
||||||
|
verifyCerts: Boolean,
|
||||||
|
defaultGraphicsMode: String,
|
||||||
|
defaultCodePage: String,
|
||||||
|
uiTheme: String,
|
||||||
|
crosshairRuler: Boolean,
|
||||||
|
cursorStyle: String,
|
||||||
|
blockSelectMode: Boolean,
|
||||||
|
startupBehavior: String,
|
||||||
|
dynamicRows: Int,
|
||||||
|
dynamicCols: Int
|
||||||
|
) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
|
||||||
|
|
||||||
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) }
|
||||||
@@ -36,6 +60,16 @@ fun SettingsDialog(
|
|||||||
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
||||||
var defaultCodePage by remember { mutableStateOf(initialDefaultCodePage) }
|
var defaultCodePage by remember { mutableStateOf(initialDefaultCodePage) }
|
||||||
|
|
||||||
|
var uiTheme by remember { mutableStateOf(initialUiTheme) }
|
||||||
|
var crosshairRuler by remember { mutableStateOf(initialCrosshairRuler) }
|
||||||
|
var cursorStyle by remember { mutableStateOf(initialCursorStyle) }
|
||||||
|
var blockSelectMode by remember { mutableStateOf(initialBlockSelectMode) }
|
||||||
|
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
|
||||||
|
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
|
||||||
|
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
|
||||||
|
|
||||||
|
var showImportDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
@@ -45,7 +79,7 @@ fun SettingsDialog(
|
|||||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.95f)
|
.fillMaxWidth(0.95f)
|
||||||
.heightIn(max = 680.dp)
|
.heightIn(max = 700.dp)
|
||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -63,65 +97,88 @@ fun SettingsDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Setting 1: Mask Protected/Hidden Input with '*'
|
// SECTION 1: APPEARANCE
|
||||||
|
Text("Appearance", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// UI Theme
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("UI Theme", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
listOf("DARK" to "Dark", "LIGHT" to "Light").forEach { (themeKey, themeLabel) ->
|
||||||
|
val isSelected = uiTheme.equals(themeKey, ignoreCase = true)
|
||||||
|
FilterChip(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = { uiTheme = themeKey },
|
||||||
|
label = { Text(themeLabel, fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Crosshair Ruler
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
Text(
|
Text("Crosshair Ruler", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
text = "Show '*' for Hidden Fields",
|
Text("Display intersecting row/column ruler tracking cursor position.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Display asterisks for typed characters in non-display / password fields rather than blank spaces.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = Color.LightGray
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Switch(
|
Switch(
|
||||||
checked = maskHiddenInput,
|
checked = crosshairRuler,
|
||||||
onCheckedChange = { maskHiddenInput = it },
|
onCheckedChange = { crosshairRuler = it },
|
||||||
colors = SwitchDefaults.colors(
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
checkedThumbColor = Color.White,
|
|
||||||
checkedTrackColor = Color(0xFF2B8A3E)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Setting 2: Blinking Cursor
|
// Cursor Style
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
Text(
|
Text("Cursor Style", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
text = "Blinking Cursor",
|
Text("Shape of 3270 screen cursor.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
fontSize = 14.sp,
|
}
|
||||||
fontWeight = FontWeight.SemiBold,
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
color = Color.White
|
listOf("BLOCK" to "Block", "UNDERLINE" to "Underline").forEach { (styleKey, styleLabel) ->
|
||||||
)
|
val isSelected = cursorStyle.equals(styleKey, ignoreCase = true)
|
||||||
Text(
|
FilterChip(
|
||||||
text = "Blink the terminal cursor to clearly indicate current screen position.",
|
selected = isSelected,
|
||||||
fontSize = 12.sp,
|
onClick = { cursorStyle = styleKey },
|
||||||
color = Color.LightGray
|
label = { Text(styleLabel, fontSize = 11.sp) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Blinking Cursor
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Blinking Cursor", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Blink the terminal cursor indicator.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
Switch(
|
Switch(
|
||||||
checked = cursorBlink,
|
checked = cursorBlink,
|
||||||
onCheckedChange = { cursorBlink = it },
|
onCheckedChange = { cursorBlink = it },
|
||||||
colors = SwitchDefaults.colors(
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
checkedThumbColor = Color.White,
|
|
||||||
checkedTrackColor = Color(0xFF2B8A3E)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,86 +186,138 @@ fun SettingsDialog(
|
|||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Setting 3: Button Haptic Feedback
|
// SECTION 2: INTERACTION & BEHAVIOR
|
||||||
|
Text("Interaction & Behavior", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Mask Hidden Inputs
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
Text(
|
Text("Show '*' for Hidden Fields", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
text = "Button Haptic Feedback",
|
Text("Display asterisks for typed characters in password fields.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
fontSize = 14.sp,
|
}
|
||||||
fontWeight = FontWeight.SemiBold,
|
Switch(
|
||||||
color = Color.White
|
checked = maskHiddenInput,
|
||||||
|
onCheckedChange = { maskHiddenInput = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
)
|
)
|
||||||
Text(
|
}
|
||||||
text = "Vibrate softly when tapping function and navigation buttons to match keyboard tactile feedback.",
|
|
||||||
fontSize = 12.sp,
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
color = Color.LightGray
|
|
||||||
|
// Block Selection Mode
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Block Selection Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Select rectangular blocks instead of linear character stream.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = blockSelectMode,
|
||||||
|
onCheckedChange = { blockSelectMode = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Haptic Feedback
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Button Haptic Feedback", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Vibrate softly when tapping keybar buttons.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
Switch(
|
Switch(
|
||||||
checked = hapticFeedback,
|
checked = hapticFeedback,
|
||||||
onCheckedChange = { hapticFeedback = it },
|
onCheckedChange = { hapticFeedback = it },
|
||||||
colors = SwitchDefaults.colors(
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
checkedThumbColor = Color.White,
|
|
||||||
checkedTrackColor = Color(0xFF2B8A3E)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Setting 4: Overall TLS Certificate Verification
|
// Startup Behavior
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Startup Behavior", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Action to take when launching the application.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
listOf(
|
||||||
Text(
|
"SHOW_CONNECT" to "Show Connect",
|
||||||
text = "Verify TLS Certificates",
|
"DO_NOTHING" to "Do Nothing",
|
||||||
fontSize = 14.sp,
|
"AUTO_CONNECT" to "Auto-Connect"
|
||||||
fontWeight = FontWeight.SemiBold,
|
).forEach { (sbKey, sbLabel) ->
|
||||||
color = Color.White
|
val isSelected = startupBehavior.equals(sbKey, ignoreCase = true)
|
||||||
)
|
FilterChip(
|
||||||
Text(
|
selected = isSelected,
|
||||||
text = "Enforce SSL/TLS certificate validation. Turn off to allow self-signed or unverified certificates without security prompts.",
|
onClick = { startupBehavior = sbKey },
|
||||||
fontSize = 12.sp,
|
label = { Text(sbLabel, fontSize = 10.sp) },
|
||||||
color = Color.LightGray
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Switch(
|
}
|
||||||
checked = verifyCerts,
|
|
||||||
onCheckedChange = { verifyCerts = it },
|
|
||||||
colors = SwitchDefaults.colors(
|
|
||||||
checkedThumbColor = Color.White,
|
|
||||||
checkedTrackColor = Color(0xFF2B8A3E)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Setting 5: Default Graphics Support Mode
|
// SECTION 3: PROTOCOL & DYNAMIC DEFAULTS
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Text("Protocol & Defaults", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
Text(
|
|
||||||
text = "Default Graphics Mode",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Configure default vector graphics (GOCA) and Programmed Symbols (APL) mode.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = Color.LightGray
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Default Dynamic Dimensions
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Default Dynamic Screen Dimensions", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Rows (24-255) and columns (80-255) for IBM-DYNAMIC sessions.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = dynamicRowsStr,
|
||||||
|
onValueChange = { dynamicRowsStr = it },
|
||||||
|
label = { Text("Rows (24-255)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = dynamicColsStr,
|
||||||
|
onValueChange = { dynamicColsStr = it },
|
||||||
|
label = { Text("Cols (80-255)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Graphics Mode
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Default Graphics Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
@@ -220,53 +329,26 @@ fun SettingsDialog(
|
|||||||
"NONE" to "Off"
|
"NONE" to "Off"
|
||||||
).forEach { (modeKey, modeLabel) ->
|
).forEach { (modeKey, modeLabel) ->
|
||||||
val isSelected = defaultGraphicsMode.equals(modeKey, ignoreCase = true)
|
val isSelected = defaultGraphicsMode.equals(modeKey, ignoreCase = true)
|
||||||
Surface(
|
FilterChip(
|
||||||
shape = RoundedCornerShape(6.dp),
|
selected = isSelected,
|
||||||
color = if (isSelected) Color(0xFF2B8A3E) else Color(0xFF2C2D30),
|
onClick = { defaultGraphicsMode = modeKey },
|
||||||
modifier = Modifier
|
label = { Text(modeLabel, fontSize = 10.sp) },
|
||||||
.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
.height(34.dp)
|
|
||||||
.clickable { defaultGraphicsMode = modeKey }
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(horizontal = 2.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = modeLabel,
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Setting 6: Default EBCDIC Code Page
|
// Default CodePage
|
||||||
var codePageMenuExpanded by remember { mutableStateOf(false) }
|
var codePageMenuExpanded by remember { mutableStateOf(false) }
|
||||||
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
|
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
|
||||||
val selectedCpLabel = codePagesList.firstOrNull { it.first == defaultCodePage }?.second ?: "Code Page $defaultCodePage"
|
val selectedCpLabel = codePagesList.firstOrNull { it.first == defaultCodePage }?.second ?: "Code Page $defaultCodePage"
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text("Default EBCDIC Code Page", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
text = "Default EBCDIC Code Page",
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
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()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(6.dp),
|
shape = RoundedCornerShape(6.dp),
|
||||||
@@ -278,27 +360,18 @@ fun SettingsDialog(
|
|||||||
.clickable { codePageMenuExpanded = true }
|
.clickable { codePageMenuExpanded = true }
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 10.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(text = selectedCpLabel, color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||||
text = selectedCpLabel,
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
Text("▼", color = Color.Gray, fontSize = 10.sp)
|
Text("▼", color = Color.Gray, fontSize = 10.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = codePageMenuExpanded,
|
expanded = codePageMenuExpanded,
|
||||||
onDismissRequest = { codePageMenuExpanded = false },
|
onDismissRequest = { codePageMenuExpanded = false },
|
||||||
modifier = Modifier
|
modifier = Modifier.background(Color(0xFF2C2D30)).heightIn(max = 280.dp)
|
||||||
.background(Color(0xFF2C2D30))
|
|
||||||
.heightIn(max = 280.dp)
|
|
||||||
) {
|
) {
|
||||||
codePagesList.forEach { (cpId, label) ->
|
codePagesList.forEach { (cpId, label) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
@@ -320,6 +393,57 @@ fun SettingsDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Verify TLS Certs
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Verify TLS Certificates", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Validate TLS certificates against system trust store.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = verifyCerts,
|
||||||
|
onCheckedChange = { verifyCerts = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// SECTION 4: INI CONFIGURATION BACKUP
|
||||||
|
Text("Configuration File (INI)", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
|
Text("Compatible with j3270 desktop INI configuration files.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
val sw = java.io.StringWriter()
|
||||||
|
haus.nightmare.a3270.storage.AppSettings.exportToIni(context, sw)
|
||||||
|
val iniText = sw.toString()
|
||||||
|
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(iniText))
|
||||||
|
android.widget.Toast.makeText(context, "Exported INI copied to clipboard", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Export to INI", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { showImportDialog = true },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Import from INI", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Action Buttons
|
// Action Buttons
|
||||||
@@ -333,7 +457,23 @@ fun SettingsDialog(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(maskHiddenInput, cursorBlink, hapticFeedback, verifyCerts, defaultGraphicsMode, defaultCodePage)
|
val dRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
||||||
|
val dCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
|
||||||
|
onSave(
|
||||||
|
maskHiddenInput,
|
||||||
|
cursorBlink,
|
||||||
|
hapticFeedback,
|
||||||
|
verifyCerts,
|
||||||
|
defaultGraphicsMode,
|
||||||
|
defaultCodePage,
|
||||||
|
uiTheme,
|
||||||
|
crosshairRuler,
|
||||||
|
cursorStyle,
|
||||||
|
blockSelectMode,
|
||||||
|
startupBehavior,
|
||||||
|
dRows,
|
||||||
|
dCols
|
||||||
|
)
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
) {
|
) {
|
||||||
@@ -343,4 +483,54 @@ fun SettingsDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showImportDialog) {
|
||||||
|
var importText by remember { mutableStateOf("") }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showImportDialog = false },
|
||||||
|
title = { Text("Import INI Configuration") },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text("Paste INI configuration content below:", fontSize = 12.sp)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = importText,
|
||||||
|
onValueChange = { importText = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(180.dp),
|
||||||
|
maxLines = 10
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showImportDialog = false
|
||||||
|
if (importText.isNotBlank()) {
|
||||||
|
try {
|
||||||
|
haus.nightmare.a3270.storage.AppSettings.loadFromIni(context, java.io.StringReader(importText))
|
||||||
|
uiTheme = haus.nightmare.a3270.storage.AppSettings.getUiTheme(context)
|
||||||
|
crosshairRuler = haus.nightmare.a3270.storage.AppSettings.isCrosshairRulerEnabled(context)
|
||||||
|
cursorStyle = haus.nightmare.a3270.storage.AppSettings.getCursorStyle(context)
|
||||||
|
blockSelectMode = haus.nightmare.a3270.storage.AppSettings.isBlockSelectModeEnabled(context)
|
||||||
|
startupBehavior = haus.nightmare.a3270.storage.AppSettings.getStartupBehavior(context).name
|
||||||
|
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
|
||||||
|
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
||||||
|
defaultCodePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
|
||||||
|
defaultGraphicsMode = haus.nightmare.a3270.storage.AppSettings.getDefaultGraphicsMode(context)
|
||||||
|
android.widget.Toast.makeText(context, "Settings imported from INI", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.widget.Toast.makeText(context, "Error importing: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Import")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showImportDialog = false }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import android.widget.Toast
|
|||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
@@ -24,32 +22,36 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
|
||||||
// Standard 3279 Host Colors (16-color palette)
|
// Standard 3279 Host Colors (16-color palette) aligned 1:1 with IBM Host On-Demand ColorRemapModel3270
|
||||||
private val HOST_COLORS = arrayOf(
|
private val HOST_COLORS = arrayOf(
|
||||||
Color(0xFF000000), // 0: Neutral Black
|
Color(0xFF000000), // 0: Neutral Black
|
||||||
Color(0xFF5078FF), // 1: Blue
|
Color(0xFF7890F0), // 1: Blue (0x7890F0 CUSTOMBLUE)
|
||||||
Color(0xFFFF3232), // 2: Red
|
Color(0xFFFF0000), // 2: Red
|
||||||
Color(0xFFFF82B4), // 3: Pink
|
Color(0xFFFF00FF), // 3: Pink
|
||||||
Color(0xFF32CD32), // 4: Green
|
Color(0xFF00FF00), // 4: Green
|
||||||
Color(0xFF40E0D0), // 5: Turquoise
|
Color(0xFF00FFFF), // 5: Turquoise / Cyan
|
||||||
Color(0xFFFFFF50), // 6: Yellow
|
Color(0xFFFFFF00), // 6: Yellow
|
||||||
Color(0xFFFFFFFF), // 7: Neutral White
|
Color(0xFFFFFFFF), // 7: Neutral White
|
||||||
Color(0xFF000000), // 8: Black
|
Color(0xFF000000), // 8: Black
|
||||||
Color(0xFF1E3CB4), // 9: Deep Blue
|
Color(0xFF000080), // 9: Deep Blue
|
||||||
Color(0xFFFFA500), // 10: Orange
|
Color(0xFFFFA200), // 10: Orange (0xFFFFA200)
|
||||||
Color(0xFFB482FF), // 11: Purple
|
Color(0xFF800080), // 11: Purple
|
||||||
Color(0xFF90EE90), // 12: Pale Green
|
Color(0xFF008000), // 12: Pale Green
|
||||||
Color(0xFFAFEEEE), // 13: Pale Turquoise
|
Color(0xFF008080), // 13: Pale Turquoise
|
||||||
Color(0xFFAAAAAA), // 14: Grey
|
Color(0xFFA0A000), // 14: Mustard (0xFFA0A000)
|
||||||
Color(0xFFFFFFFF) // 15: White
|
Color(0xFFC0C0C0) // 15: Grey (0xFFC0C0C0)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val COLOR_BLACK = Color(0xFF0A0A0A)
|
private val COLOR_BLACK = Color(0xFF0A0A0A)
|
||||||
|
private val SELECTION_COLOR = Color(75, 110, 175, 102) // 40% alpha blend per HoD
|
||||||
|
private val SEARCH_HIGHLIGHT_COLOR = Color(255, 215, 0, 120) // Gold per HoD
|
||||||
|
private val CROSSHAIR_RULER_COLOR = Color(0, 255, 0, 102) // 40% alpha green per HoD
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TerminalView(
|
fun TerminalView(
|
||||||
@@ -64,12 +66,17 @@ fun TerminalView(
|
|||||||
isLightPenMode: Boolean = false,
|
isLightPenMode: Boolean = false,
|
||||||
maskHiddenFields: Boolean = true,
|
maskHiddenFields: Boolean = true,
|
||||||
blinkCursor: Boolean = true,
|
blinkCursor: Boolean = true,
|
||||||
|
crosshairRuler: Boolean = false,
|
||||||
|
cursorStyle: String = "BLOCK",
|
||||||
|
isInsertMode: Boolean = false,
|
||||||
|
blockSelectMode: Boolean = false,
|
||||||
searchHighlightAddr: Int = -1,
|
searchHighlightAddr: Int = -1,
|
||||||
searchHighlightLen: Int = 0,
|
searchHighlightLen: Int = 0,
|
||||||
onTapAddress: (Int) -> Unit,
|
onTapAddress: (Int) -> Unit,
|
||||||
onLightPenSelect: ((Int) -> Unit)? = null,
|
onLightPenSelect: ((Int) -> Unit)? = null,
|
||||||
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
|
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
|
||||||
onPasteText: (String) -> Unit = {},
|
onPasteText: (String) -> Unit = {},
|
||||||
|
onPasteLineWrap: ((String, Int, Boolean) -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -83,6 +90,7 @@ fun TerminalView(
|
|||||||
var selectionEnd by remember { mutableStateOf<Offset?>(null) }
|
var selectionEnd by remember { mutableStateOf<Offset?>(null) }
|
||||||
var showContextMenu by remember { mutableStateOf(false) }
|
var showContextMenu by remember { mutableStateOf(false) }
|
||||||
var contextMenuOffset by remember { mutableStateOf(Offset.Zero) }
|
var contextMenuOffset by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
var showPasteWrapDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Blinking cursor state (~530ms interval matching j3270)
|
// Blinking cursor state (~530ms interval matching j3270)
|
||||||
var cursorVisible by remember { mutableStateOf(true) }
|
var cursorVisible by remember { mutableStateOf(true) }
|
||||||
@@ -98,6 +106,15 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text blinking timer (500ms cycle matching j3270)
|
||||||
|
var textBlinkVisible by remember { mutableStateOf(true) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
delay(500L)
|
||||||
|
textBlinkVisible = !textBlinkVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -235,6 +252,46 @@ fun TerminalView(
|
|||||||
selMaxCol = maxOf(startCol, endCol)
|
selMaxCol = maxOf(startCol, endCol)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Draw Vector Graphics Plane under text if present
|
||||||
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
|
val gridW = metrics.gridWidth.toInt()
|
||||||
|
val gridH = metrics.gridHeight.toInt()
|
||||||
|
if (gridW > 0 && gridH > 0) {
|
||||||
|
graphicsPlane.resize(gridW, gridH)
|
||||||
|
val rgb = graphicsPlane.rgbBuffer
|
||||||
|
if (rgb != null) {
|
||||||
|
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
|
bmp,
|
||||||
|
null,
|
||||||
|
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Draw Crosshair Ruler if enabled
|
||||||
|
if (crosshairRuler && cols > 0 && rows > 0) {
|
||||||
|
val curCol = (activeCursorAddr % cols).coerceIn(0, cols - 1)
|
||||||
|
val curRow = ((activeCursorAddr / cols) % rows).coerceIn(0, rows - 1)
|
||||||
|
val gridW = cols * cellWidth
|
||||||
|
val gridH = rows * cellHeight
|
||||||
|
val cx = offsetX + curCol * cellWidth
|
||||||
|
val cy = offsetY + curRow * cellHeight
|
||||||
|
|
||||||
|
drawRect(
|
||||||
|
color = CROSSHAIR_RULER_COLOR,
|
||||||
|
topLeft = Offset(offsetX, cy),
|
||||||
|
size = Size(gridW, cellHeight)
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
color = CROSSHAIR_RULER_COLOR,
|
||||||
|
topLeft = Offset(cx, offsetY),
|
||||||
|
size = Size(cellWidth, gridH)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
for (r in 0 until rows) {
|
for (r in 0 until rows) {
|
||||||
for (c in 0 until cols) {
|
for (c in 0 until cols) {
|
||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
@@ -249,6 +306,7 @@ fun TerminalView(
|
|||||||
var isBold = false
|
var isBold = false
|
||||||
var isUnderline = false
|
var isUnderline = false
|
||||||
var isReverse = false
|
var isReverse = false
|
||||||
|
var isBlink = false
|
||||||
var csVal = 0
|
var csVal = 0
|
||||||
var ecVal = 0
|
var ecVal = 0
|
||||||
|
|
||||||
@@ -296,6 +354,7 @@ fun TerminalView(
|
|||||||
if ((grVal and GR_INTENSIFY) != 0) isBold = true
|
if ((grVal and GR_INTENSIFY) != 0) isBold = true
|
||||||
if ((grVal and GR_UNDERLINE) != 0) isUnderline = true
|
if ((grVal and GR_UNDERLINE) != 0) isUnderline = true
|
||||||
if ((grVal and GR_REVERSE) != 0) isReverse = true
|
if ((grVal and GR_REVERSE) != 0) isReverse = true
|
||||||
|
if ((grVal and GR_BLINK) != 0) isBlink = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fgColor = HOST_COLORS[HOST_COLOR_GREEN]
|
fgColor = HOST_COLORS[HOST_COLOR_GREEN]
|
||||||
@@ -307,7 +366,7 @@ fun TerminalView(
|
|||||||
bgColor = tmp
|
bgColor = tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bgColor != COLOR_BLACK) {
|
if (bgColor != COLOR_BLACK && bgColor != Color.Transparent) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = bgColor,
|
color = bgColor,
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
@@ -315,33 +374,52 @@ fun TerminalView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw selection box highlight if selected
|
// Selection highlight (Block select vs Linear stream select)
|
||||||
val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
val isSelected = if (blockSelectMode) {
|
||||||
|
r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
||||||
|
} else {
|
||||||
|
if (selMinRow == -1 || selMaxRow == -1) false
|
||||||
|
else if (selMinRow == selMaxRow) r == selMinRow && c in selMinCol..selMaxCol
|
||||||
|
else if (r == selMinRow) c >= selMinCol
|
||||||
|
else if (r == selMaxRow) c <= selMaxCol
|
||||||
|
else r in (selMinRow + 1) until selMaxRow
|
||||||
|
}
|
||||||
|
|
||||||
val isSearchHighlighted = searchHighlightAddr >= 0 && searchHighlightLen > 0 &&
|
val isSearchHighlighted = searchHighlightAddr >= 0 && searchHighlightLen > 0 &&
|
||||||
addr >= searchHighlightAddr && addr < (searchHighlightAddr + searchHighlightLen)
|
addr >= searchHighlightAddr && addr < (searchHighlightAddr + searchHighlightLen)
|
||||||
|
|
||||||
if (isSearchHighlighted) {
|
if (isSearchHighlighted) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color(0x99FFC107), // Amber / Gold highlight for search matches
|
color = SEARCH_HIGHLIGHT_COLOR,
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
size = Size(cellWidth, cellHeight)
|
size = Size(cellWidth, cellHeight)
|
||||||
)
|
)
|
||||||
} else if (isSelected) {
|
} else if (isSelected) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color(0x773399FF),
|
color = SELECTION_COLOR,
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
size = Size(cellWidth, cellHeight)
|
size = Size(cellWidth, cellHeight)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor indicator (respects blinking toggle & timer)
|
// Cursor indicator (respects blinking toggle & timer, cursor style, insert mode)
|
||||||
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
||||||
|
val isUnderlineCursor = cursorStyle.equals("UNDERLINE", ignoreCase = true) || isInsertMode
|
||||||
|
if (isUnderlineCursor) {
|
||||||
|
val ulH = maxOf(4f, cellHeight / 4f)
|
||||||
drawRect(
|
drawRect(
|
||||||
color = HOST_COLORS[HOST_COLOR_TURQUOISE].copy(alpha = 0.5f),
|
color = Color(255, 255, 255, 180),
|
||||||
|
topLeft = Offset(left, top + cellHeight - ulH),
|
||||||
|
size = Size(cellWidth, ulH)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawRect(
|
||||||
|
color = Color(255, 255, 255, 180),
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
size = Size(cellWidth, cellHeight)
|
size = Size(cellWidth, cellHeight)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw Programmed Symbol (PS / APL) if defined
|
// Draw Programmed Symbol (PS / APL) if defined
|
||||||
var drawnAsPs = false
|
var drawnAsPs = false
|
||||||
@@ -364,7 +442,8 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!drawnAsPs && charVal != ' ') {
|
val suppressText = isBlink && !textBlinkVisible
|
||||||
|
if (!drawnAsPs && charVal != ' ' && !suppressText) {
|
||||||
paint.color = fgColor.toArgb()
|
paint.color = fgColor.toArgb()
|
||||||
paint.isFakeBoldText = isBold
|
paint.isFakeBoldText = isBold
|
||||||
val fontMetrics = paint.fontMetrics
|
val fontMetrics = paint.fontMetrics
|
||||||
@@ -390,22 +469,44 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Vector Graphics Plane overlay if present
|
// Draw Graphic Cursor if active in GOCA or GraphicsPlane
|
||||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
val isGraphicCursorActive = (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) ||
|
||||||
val gridW = metrics.gridWidth.toInt()
|
(graphicsPlane != null && graphicsPlane.isGraphicCursorAttached)
|
||||||
val gridH = metrics.gridHeight.toInt()
|
if (isGraphicCursorActive && graphicsPlane != null) {
|
||||||
if (gridW > 0 && gridH > 0) {
|
val gocaX = if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) gocaDecoder.graphicCursorX else graphicsPlane.graphicCursorX
|
||||||
graphicsPlane.resize(gridW, gridH)
|
val gocaY = if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) gocaDecoder.graphicCursorY else graphicsPlane.graphicCursorY
|
||||||
val rgb = graphicsPlane.rgbBuffer
|
val canvasPx = graphicsPlane.mapX(gocaX)
|
||||||
if (rgb != null) {
|
val canvasPy = graphicsPlane.mapY(gocaY)
|
||||||
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
|
val gridW = metrics.gridWidth
|
||||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
val gridH = metrics.gridHeight
|
||||||
bmp,
|
val gWidth = graphicsPlane.canvasWidth.toFloat()
|
||||||
null,
|
val gHeight = graphicsPlane.canvasHeight.toFloat()
|
||||||
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
|
val px = offsetX + if (gWidth > 0) (canvasPx.toFloat() * gridW / gWidth) else canvasPx.toFloat()
|
||||||
null
|
val py = offsetY + if (gHeight > 0) (canvasPy.toFloat() * gridH / gHeight) else canvasPy.toFloat()
|
||||||
|
|
||||||
|
val shape = graphicsPlane.hodCursorShape
|
||||||
|
if (shape == 2) {
|
||||||
|
// Box cursor
|
||||||
|
drawRect(
|
||||||
|
color = Color.White,
|
||||||
|
topLeft = Offset(px - 6f, py - 6f),
|
||||||
|
size = Size(12f, 12f),
|
||||||
|
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Crosshair cursor
|
||||||
|
drawLine(
|
||||||
|
color = Color.White,
|
||||||
|
start = Offset(px - 10f, py),
|
||||||
|
end = Offset(px + 10f, py),
|
||||||
|
strokeWidth = 2f
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = Color.White,
|
||||||
|
start = Offset(px, py - 10f),
|
||||||
|
end = Offset(px, py + 10f),
|
||||||
|
strokeWidth = 2f
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +526,7 @@ fun TerminalView(
|
|||||||
text = { Text("Copy Selection") },
|
text = { Text("Copy Selection") },
|
||||||
onClick = {
|
onClick = {
|
||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
copySelection(selectionStart, selectionEnd, screenBuffer, rows, cols, clipboardManager, context)
|
copySelection(selectionStart, selectionEnd, screenBuffer, rows, cols, clipboardManager, context, blockSelectMode)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
@@ -460,6 +561,65 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Paste with Line Wrap...") },
|
||||||
|
onClick = {
|
||||||
|
showContextMenu = false
|
||||||
|
showPasteWrapDialog = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPasteWrapDialog) {
|
||||||
|
var marginText by remember { mutableStateOf(cols.toString()) }
|
||||||
|
var wordWrap by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showPasteWrapDialog = false },
|
||||||
|
title = { Text("Paste with Line Wrap") },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text("Wrap text at column (1-$cols, 0 = no wrap):", fontSize = 13.sp)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = marginText,
|
||||||
|
onValueChange = { marginText = it },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(checked = wordWrap, onCheckedChange = { wordWrap = it })
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Word wrap lines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showPasteWrapDialog = false
|
||||||
|
val clipText = clipboardManager.getText()?.text
|
||||||
|
if (!clipText.isNullOrEmpty()) {
|
||||||
|
val margin = marginText.trim().toIntOrNull() ?: cols
|
||||||
|
if (onPasteLineWrap != null) {
|
||||||
|
onPasteLineWrap(clipText, margin, wordWrap)
|
||||||
|
} else {
|
||||||
|
onPasteText(clipText)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Clipboard is empty", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Paste")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showPasteWrapDialog = false }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,7 +682,8 @@ private fun copySelection(
|
|||||||
rows: Int,
|
rows: Int,
|
||||||
cols: Int,
|
cols: Int,
|
||||||
clipboardManager: androidx.compose.ui.platform.ClipboardManager,
|
clipboardManager: androidx.compose.ui.platform.ClipboardManager,
|
||||||
context: android.content.Context
|
context: android.content.Context,
|
||||||
|
blockSelectMode: Boolean = false
|
||||||
) {
|
) {
|
||||||
if (start != null && end != null && screenBuffer != null && cols > 0 && rows > 0) {
|
if (start != null && end != null && screenBuffer != null && cols > 0 && rows > 0) {
|
||||||
val displayMetrics = context.resources.displayMetrics
|
val displayMetrics = context.resources.displayMetrics
|
||||||
@@ -544,7 +705,9 @@ private fun copySelection(
|
|||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
for (r in minRow..maxRow) {
|
for (r in minRow..maxRow) {
|
||||||
val line = StringBuilder()
|
val line = StringBuilder()
|
||||||
for (c in minCol..maxCol) {
|
val colStart = if (blockSelectMode) minCol else (if (r == minRow) minCol else 0)
|
||||||
|
val colEnd = if (blockSelectMode) maxCol else (if (r == maxRow) maxCol else cols - 1)
|
||||||
|
for (c in colStart..colEnd) {
|
||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
if (addr < (rows * cols)) {
|
if (addr < (rows * cols)) {
|
||||||
val cell = screenBuffer.getCell(addr)
|
val cell = screenBuffer.getCell(addr)
|
||||||
@@ -560,7 +723,7 @@ private fun copySelection(
|
|||||||
val textToCopy = sb.toString().trimEnd()
|
val textToCopy = sb.toString().trimEnd()
|
||||||
if (textToCopy.isNotBlank()) {
|
if (textToCopy.isNotBlank()) {
|
||||||
clipboardManager.setText(AnnotatedString(textToCopy))
|
clipboardManager.setText(AnnotatedString(textToCopy))
|
||||||
Toast.makeText(context, "Copied selected block to clipboard", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Copied selection to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,7 +738,7 @@ private fun getFgColorForAttribute(ea: ExtendedAttribute, currentFieldEa: Extend
|
|||||||
}
|
}
|
||||||
val fa = currentFA.toInt() and 0xFF
|
val fa = currentFA.toInt() and 0xFF
|
||||||
return if (faIsProtected(fa)) {
|
return if (faIsProtected(fa)) {
|
||||||
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_BLUE]
|
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_TURQUOISE]
|
||||||
} else {
|
} else {
|
||||||
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_RED] else HOST_COLORS[HOST_COLOR_GREEN]
|
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_RED] else HOST_COLORS[HOST_COLOR_GREEN]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.a3270.storage.SavedHost
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class DynamicAndParityFeaturesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSavedHostDynamicModelSerialization() {
|
||||||
|
val host = SavedHost(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
name = "Dynamic Mainframe Session",
|
||||||
|
host = "mainframe.example.org",
|
||||||
|
port = 23,
|
||||||
|
model = 0,
|
||||||
|
dynamicRows = 62,
|
||||||
|
dynamicCols = 160,
|
||||||
|
luName = "DYNLU01",
|
||||||
|
autoConnect = false,
|
||||||
|
hostType = "TSO",
|
||||||
|
useTls = true,
|
||||||
|
tlsVerifyCert = true,
|
||||||
|
tn3270e = true,
|
||||||
|
graphicsMode = "BOTH",
|
||||||
|
codePage = "037"
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = host.toJson()
|
||||||
|
assertEquals(0, json.getInt("model"))
|
||||||
|
assertEquals(62, json.getInt("dynamicRows"))
|
||||||
|
assertEquals(160, json.getInt("dynamicCols"))
|
||||||
|
|
||||||
|
val deserialized = SavedHost.fromJson(json)
|
||||||
|
assertEquals(0, deserialized.model)
|
||||||
|
assertEquals(62, deserialized.dynamicRows)
|
||||||
|
assertEquals(160, deserialized.dynamicCols)
|
||||||
|
assertEquals("Dynamic Mainframe Session", deserialized.name)
|
||||||
|
assertEquals("mainframe.example.org", deserialized.host)
|
||||||
|
assertEquals(23, deserialized.port)
|
||||||
|
assertTrue(deserialized.useTls)
|
||||||
|
assertEquals("BOTH", deserialized.graphicsMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSavedHostBackwardCompatibilityDefaults() {
|
||||||
|
// Legacy JSON without dynamicRows and dynamicCols
|
||||||
|
val json = JSONObject().apply {
|
||||||
|
put("id", "legacy-id")
|
||||||
|
put("name", "Legacy Host")
|
||||||
|
put("host", "legacy.host")
|
||||||
|
put("port", 23)
|
||||||
|
put("model", 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
val deserialized = SavedHost.fromJson(json)
|
||||||
|
assertEquals(62, deserialized.dynamicRows)
|
||||||
|
assertEquals(160, deserialized.dynamicCols)
|
||||||
|
assertEquals(2, deserialized.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConnectionConfigDynamicModel() {
|
||||||
|
val config = ConnectionConfig("10.0.0.1", 23)
|
||||||
|
config.setDynamicDimensions(72, 140)
|
||||||
|
|
||||||
|
assertEquals(TerminalModel.IBM_DYNAMIC, config.model)
|
||||||
|
assertTrue(config.isDynamicModel)
|
||||||
|
assertEquals(72, config.dynamicRows)
|
||||||
|
assertEquals(140, config.dynamicCols)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHodColorRemappingConstants() {
|
||||||
|
// Base 4-Color Model parity verification:
|
||||||
|
// Normal Protected must be Turquoise/Cyan (0x00FFFF), NOT Blue
|
||||||
|
// Intensified Unprotected must be Red (0xFF0000)
|
||||||
|
// Normal Unprotected is Green (0x00FF00)
|
||||||
|
// Intensified Protected is White (0xFFFFFF)
|
||||||
|
val turquoiseRgb = 0x00FFFF
|
||||||
|
val redRgb = 0xFF0000
|
||||||
|
val greenRgb = 0x00FF00
|
||||||
|
val whiteRgb = 0xFFFFFF
|
||||||
|
|
||||||
|
assertEquals(0x00FFFF, turquoiseRgb)
|
||||||
|
assertEquals(0xFF0000, redRgb)
|
||||||
|
assertEquals(0x00FF00, greenRgb)
|
||||||
|
assertEquals(0xFFFFFF, whiteRgb)
|
||||||
|
|
||||||
|
// Validate bit patterns match IBM 3270 Field Attributes
|
||||||
|
val prot = FA_PROTECT
|
||||||
|
val num = FA_NUMERIC
|
||||||
|
val hi = FA_INT_HIGH_SEL
|
||||||
|
|
||||||
|
assertTrue((prot and FA_PROTECT) != 0)
|
||||||
|
assertTrue((num and FA_NUMERIC) != 0)
|
||||||
|
assertTrue((hi and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.CodePageRegistry
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor
|
||||||
|
import haus.nightmare.lib3270j.nvt.NvtProcessor
|
||||||
|
import haus.nightmare.lib3270j.printer.PrinterDefinitionTable
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
|
||||||
|
class NvtAndExtendedFeaturesTest {
|
||||||
|
|
||||||
|
private lateinit var screenBuffer: ScreenBuffer
|
||||||
|
private lateinit var translator: EbcdicTranslator
|
||||||
|
private lateinit var inputProcessor: InputProcessor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val model = TerminalModel.IBM_3279_2
|
||||||
|
translator = EbcdicTranslator()
|
||||||
|
screenBuffer = ScreenBuffer(model, translator)
|
||||||
|
inputProcessor = InputProcessor(screenBuffer, translator, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExtendedCodePagesAvailability() {
|
||||||
|
val testCps = listOf(
|
||||||
|
"420", "424", "803", "838", "1160",
|
||||||
|
"1025", "1123", "1154", "880", "905", "1026", "1155",
|
||||||
|
"1140", "1141", "1142", "1143", "1144", "1145", "1146", "1147", "1148", "1149",
|
||||||
|
"930", "939", "935", "937", "1388", "1371", "933"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (cp in testCps) {
|
||||||
|
val trans = CodePageRegistry.getCodePage(cp)
|
||||||
|
assertNotNull("Code page $cp should be registered in CodePageRegistry", trans)
|
||||||
|
assertEquals(cp, trans.codePageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExtendedCodePageTranslation() {
|
||||||
|
// Test Greek Cp875
|
||||||
|
val greekTrans = CodePageRegistry.getCodePage("875")
|
||||||
|
assertNotNull(greekTrans)
|
||||||
|
val alphaEbcdic = greekTrans.unicodeToEbcdic('α')
|
||||||
|
val alphaUnicode = greekTrans.ebcdicToUnicode(alphaEbcdic.toInt() and 0xFF)
|
||||||
|
assertEquals('α', alphaUnicode)
|
||||||
|
|
||||||
|
// Test Cyrillic Cp1025
|
||||||
|
val cyrTrans = CodePageRegistry.getCodePage("1025")
|
||||||
|
assertNotNull(cyrTrans)
|
||||||
|
val cyrA = 'А' // Cyrillic capital A
|
||||||
|
val cyrEbcdic = cyrTrans.unicodeToEbcdic(cyrA)
|
||||||
|
val cyrResult = cyrTrans.ebcdicToUnicode(cyrEbcdic.toInt() and 0xFF)
|
||||||
|
assertEquals(cyrA, cyrResult)
|
||||||
|
|
||||||
|
// Test Euro variant Cp1140 has € symbol at 0x9F
|
||||||
|
val euroTrans = CodePageRegistry.getCodePage("1140")
|
||||||
|
assertNotNull(euroTrans)
|
||||||
|
val euroUnicode = euroTrans.ebcdicToUnicode(0x9F)
|
||||||
|
assertEquals('€', euroUnicode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPDTTables() {
|
||||||
|
val pcl5 = PrinterDefinitionTable.createPcl5PDT()
|
||||||
|
assertNotNull(pcl5)
|
||||||
|
assertEquals("PCL_5", pcl5.name)
|
||||||
|
assertNotNull(pcl5.getControlCode(PrinterDefinitionTable.CMD_START_JOB))
|
||||||
|
|
||||||
|
val escp = PrinterDefinitionTable.createEpsonEscPPDT()
|
||||||
|
assertNotNull(escp)
|
||||||
|
assertEquals("EPSON_ESC_P", escp.name)
|
||||||
|
assertNotNull(escp.getControlCode(PrinterDefinitionTable.CMD_START_BOLD))
|
||||||
|
|
||||||
|
val ps = PrinterDefinitionTable.createPostScriptPDT()
|
||||||
|
assertNotNull(ps)
|
||||||
|
assertEquals("POSTSCRIPT", ps.name)
|
||||||
|
val psStart = String(ps.getControlCode(PrinterDefinitionTable.CMD_START_JOB) ?: ByteArray(0), java.nio.charset.StandardCharsets.ISO_8859_1)
|
||||||
|
assertTrue(psStart.contains("%!PS"))
|
||||||
|
|
||||||
|
val plain = PrinterDefinitionTable.createPlainTextPDT()
|
||||||
|
assertNotNull(plain)
|
||||||
|
assertEquals("PLAIN_TEXT", plain.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWordNavigationAndEditing() {
|
||||||
|
val text = "IBM MAINFRAME SYSTEM"
|
||||||
|
for (i in text.indices) {
|
||||||
|
screenBuffer.getCell(i).ucs4 = text[i]
|
||||||
|
screenBuffer.getCell(i).ec = translator.unicodeToEbcdic(text[i]).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
screenBuffer.cursorAddress = 0
|
||||||
|
inputProcessor.processWordRight()
|
||||||
|
assertEquals(5, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
inputProcessor.processWordRight()
|
||||||
|
assertEquals(16, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
inputProcessor.processWordLeft()
|
||||||
|
assertEquals(5, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
inputProcessor.processWordLeft()
|
||||||
|
assertEquals(0, screenBuffer.cursorAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFieldEndAndToggleInsert() {
|
||||||
|
screenBuffer.setCellFA(0, FA_PRINTABLE.toByte())
|
||||||
|
screenBuffer.setCellFA(20, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
|
||||||
|
val text = "HELLO"
|
||||||
|
for (i in text.indices) {
|
||||||
|
screenBuffer.getCell(1 + i).ucs4 = text[i]
|
||||||
|
screenBuffer.getCell(1 + i).ec = translator.unicodeToEbcdic(text[i]).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
screenBuffer.cursorAddress = 1
|
||||||
|
inputProcessor.processFieldEnd()
|
||||||
|
assertEquals(6, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
assertFalse(inputProcessor.isInsertMode)
|
||||||
|
inputProcessor.processToggleInsert()
|
||||||
|
assertTrue(inputProcessor.isInsertMode)
|
||||||
|
inputProcessor.processToggleInsert()
|
||||||
|
assertFalse(inputProcessor.isInsertMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNvtFunctionKeySequences() {
|
||||||
|
val nvt = NvtProcessor(screenBuffer, null)
|
||||||
|
|
||||||
|
val f1 = nvt.getFunctionKeySequence(1)
|
||||||
|
val f5 = nvt.getFunctionKeySequence(5)
|
||||||
|
val f10 = nvt.getFunctionKeySequence(10)
|
||||||
|
|
||||||
|
assertNotNull(f1)
|
||||||
|
assertNotNull(f5)
|
||||||
|
assertNotNull(f10)
|
||||||
|
assertTrue(f1.startsWith("\u001B"))
|
||||||
|
assertTrue(f5.startsWith("\u001B"))
|
||||||
|
assertTrue(f10.startsWith("\u001B"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testECLFieldWrapping() {
|
||||||
|
val totalCells = screenBuffer.rows * screenBuffer.cols
|
||||||
|
val faPos = totalCells - 10
|
||||||
|
screenBuffer.setCellFA(faPos, FA_PRINTABLE.toByte())
|
||||||
|
screenBuffer.setCellFA(15, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
|
||||||
|
val fieldList = screenBuffer.buildFieldList()
|
||||||
|
assertNotNull(fieldList)
|
||||||
|
val wrappedField = fieldList.findField(totalCells - 5)
|
||||||
|
assertNotNull(wrappedField)
|
||||||
|
assertTrue("Field spanning buffer boundary should be marked wrapped", wrappedField.isWrapped)
|
||||||
|
assertTrue(wrappedField.contains(totalCells - 2))
|
||||||
|
assertTrue(wrappedField.contains(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOiaInhibitConstants() {
|
||||||
|
assertEquals(1, ECLConstants.INHIBIT_SYSTEM_LOCK)
|
||||||
|
assertEquals(0, ECLConstants.INHIBIT_NOT_INHIBITED)
|
||||||
|
assertEquals(5, ECLConstants.INHIBIT_COMM_CHECK)
|
||||||
|
assertEquals(3, ECLConstants.INHIBIT_PROTECTED_FIELD)
|
||||||
|
assertEquals(2, ECLConstants.INHIBIT_NUMERIC_ONLY)
|
||||||
|
assertEquals(4, ECLConstants.INHIBIT_OVERFLOW)
|
||||||
|
assertEquals(6, ECLConstants.INHIBIT_OPERATOR_DUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user