Update Status Bar, Settings, Terminal alignment and codepage testing
Build and Test a3270 / Build Android APK (push) Successful in 1m58s
Build and Test a3270 / Build Android APK (push) Successful in 1m58s
This commit is contained in:
@@ -546,11 +546,34 @@ fun MainScreen(
|
|||||||
val hotspotsPfEnabled by viewModel.hotspotsPfEnabled.collectAsState()
|
val hotspotsPfEnabled by viewModel.hotspotsPfEnabled.collectAsState()
|
||||||
val hotspotsMenuChoicesEnabled by viewModel.hotspotsMenuChoicesEnabled.collectAsState()
|
val hotspotsMenuChoicesEnabled by viewModel.hotspotsMenuChoicesEnabled.collectAsState()
|
||||||
|
|
||||||
val isNumericField = remember(screenBuffer, cursorAddr, screenVersion) {
|
val isDbcsSession = remember(screenBuffer, screenVersion) {
|
||||||
if (screenBuffer != null && screenBuffer?.isFormatted == true) {
|
screenBuffer?.translator?.isDBCS == true
|
||||||
val fa = screenBuffer?.getFieldAttributeAt(cursorAddr) ?: 0.toByte()
|
}
|
||||||
(fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0
|
|
||||||
} else false
|
val fieldStatus = remember(screenBuffer, cursorAddr, screenVersion) {
|
||||||
|
val buf = screenBuffer
|
||||||
|
if (buf != null && buf.isFormatted) {
|
||||||
|
val cell = buf.getCell(cursorAddr)
|
||||||
|
val isDbcs = (cell != null && (cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_LEFT ||
|
||||||
|
cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_RIGHT ||
|
||||||
|
cell.cs == haus.nightmare.lib3270j.screen.ExtendedAttribute.CS_DBCS))
|
||||||
|
if (isDbcs) {
|
||||||
|
"DBCS"
|
||||||
|
} else {
|
||||||
|
val fa = buf.getFieldAttributeAt(cursorAddr)
|
||||||
|
if ((fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0) {
|
||||||
|
"NUM"
|
||||||
|
} else {
|
||||||
|
"ALPHA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"ALPHA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isNumericField = remember(fieldStatus) {
|
||||||
|
fieldStatus == "NUM"
|
||||||
}
|
}
|
||||||
|
|
||||||
var showConnectDialog by remember { mutableStateOf(false) }
|
var showConnectDialog by remember { mutableStateOf(false) }
|
||||||
@@ -717,6 +740,7 @@ fun MainScreen(
|
|||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
TerminalInputView(ctx).apply {
|
TerminalInputView(ctx).apply {
|
||||||
|
this.isDbcsSession = isDbcsSession
|
||||||
onInputText = { text ->
|
onInputText = { text ->
|
||||||
viewModel.typeString(text)
|
viewModel.typeString(text)
|
||||||
onClearShift()
|
onClearShift()
|
||||||
@@ -738,6 +762,7 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { inputView ->
|
update = { inputView ->
|
||||||
|
inputView.isDbcsSession = isDbcsSession
|
||||||
terminalInputViewRef = inputView
|
terminalInputViewRef = inputView
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(1.dp)
|
modifier = Modifier.size(1.dp)
|
||||||
@@ -880,6 +905,7 @@ fun MainScreen(
|
|||||||
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
||||||
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
||||||
isNumericField = isNumericField,
|
isNumericField = isNumericField,
|
||||||
|
fieldStatus = fieldStatus,
|
||||||
uiTheme = uiTheme,
|
uiTheme = uiTheme,
|
||||||
onToggleDocMode = { viewModel.toggleDocMode() },
|
onToggleDocMode = { viewModel.toggleDocMode() },
|
||||||
onToggleWordWrap = { viewModel.toggleWordWrap() },
|
onToggleWordWrap = { viewModel.toggleWordWrap() },
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ object HostStorage {
|
|||||||
"1149" to "1149 - Iceland (Euro €)",
|
"1149" to "1149 - Iceland (Euro €)",
|
||||||
"930" to "930 - Japanese Katakana Mixed DBCS",
|
"930" to "930 - Japanese Katakana Mixed DBCS",
|
||||||
"939" to "939 - Japanese Latin Mixed DBCS",
|
"939" to "939 - Japanese Latin Mixed DBCS",
|
||||||
|
"290" to "290 - Japanese Katakana SBCS",
|
||||||
|
"1027" to "1027 - Japanese Latin Extended SBCS",
|
||||||
|
"1390" to "1390 - Japanese Katakana DBCS Euro",
|
||||||
|
"1399" to "1399 - Japanese Latin DBCS Euro",
|
||||||
"935" to "935 - Simplified Chinese Mixed DBCS",
|
"935" to "935 - Simplified Chinese Mixed DBCS",
|
||||||
"937" to "937 - Traditional Chinese Mixed DBCS",
|
"937" to "937 - Traditional Chinese Mixed DBCS",
|
||||||
"1388" to "1388 - Simplified Chinese Extended DBCS",
|
"1388" to "1388 - Simplified Chinese Extended DBCS",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ fun OiaStatusBar(
|
|||||||
inhibitReason: Int = 0,
|
inhibitReason: Int = 0,
|
||||||
luName: String = "",
|
luName: String = "",
|
||||||
isNumericField: Boolean = false,
|
isNumericField: Boolean = false,
|
||||||
|
fieldStatus: String = "",
|
||||||
uiTheme: String = "DARK",
|
uiTheme: String = "DARK",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -163,16 +164,22 @@ fun OiaStatusBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field Status: NUM vs ALPHA
|
// Field Status: DBCS vs NUM vs ALPHA
|
||||||
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
||||||
|
val effectiveFieldStatus = if (fieldStatus.isNotBlank()) fieldStatus else (if (isNumericField) "NUM" else "ALPHA")
|
||||||
|
val badgeColor = when (effectiveFieldStatus) {
|
||||||
|
"DBCS" -> sysAvailClr
|
||||||
|
"NUM" -> Color(0xFFFFD43B)
|
||||||
|
else -> Color(0xFF74C0FC)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Surface(
|
Surface(
|
||||||
color = Color(0xFF339AF0).copy(alpha = 0.2f),
|
color = badgeColor.copy(alpha = 0.2f),
|
||||||
shape = RoundedCornerShape(4.dp)
|
shape = RoundedCornerShape(4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (isNumericField) "NUM" else "ALPHA",
|
text = effectiveFieldStatus,
|
||||||
color = if (isNumericField) Color(0xFFFFD43B) else Color(0xFF74C0FC),
|
color = badgeColor,
|
||||||
fontSize = 9.sp,
|
fontSize = 9.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.core.content.FileProvider
|
|||||||
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
||||||
import haus.nightmare.lib3270j.graphics.ProgramSymbolManager
|
import haus.nightmare.lib3270j.graphics.ProgramSymbolManager
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -296,6 +297,10 @@ private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int,
|
|||||||
val faVal = cell.fa.toInt() and 0xFF
|
val faVal = cell.fa.toInt() and 0xFF
|
||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
rowSb.append(' ')
|
rowSb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
|
if (c > 0) continue else rowSb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_SO || cell.db == ExtendedAttribute.DB_SI) {
|
||||||
|
rowSb.append(' ')
|
||||||
} else {
|
} else {
|
||||||
if (currentHidden && maskHidden) {
|
if (currentHidden && maskHidden) {
|
||||||
rowSb.append(' ')
|
rowSb.append(' ')
|
||||||
@@ -346,6 +351,10 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
|
|||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
currentFieldEa = cell
|
currentFieldEa = cell
|
||||||
sb.append(' ')
|
sb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
|
if (c > 0) continue else sb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_SO || cell.db == ExtendedAttribute.DB_SI) {
|
||||||
|
sb.append(' ')
|
||||||
} else {
|
} else {
|
||||||
if (currentHidden && maskHidden) {
|
if (currentHidden && maskHidden) {
|
||||||
sb.append(' ')
|
sb.append(' ')
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ fun SettingsDialog(
|
|||||||
var hotspotsPfEnabled by remember { mutableStateOf(initialHotspotsPfEnabled) }
|
var hotspotsPfEnabled by remember { mutableStateOf(initialHotspotsPfEnabled) }
|
||||||
var hotspotsMenuChoicesEnabled by remember { mutableStateOf(initialHotspotsMenuChoicesEnabled) }
|
var hotspotsMenuChoicesEnabled by remember { mutableStateOf(initialHotspotsMenuChoicesEnabled) }
|
||||||
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
|
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
|
||||||
|
val savedHosts = remember { haus.nightmare.a3270.storage.HostStorage.getSavedHosts(context) }
|
||||||
|
var selectedAutoHostId by remember {
|
||||||
|
mutableStateOf(haus.nightmare.a3270.storage.HostStorage.getAutoConnectHost(context)?.id ?: savedHosts.firstOrNull()?.id ?: "")
|
||||||
|
}
|
||||||
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
|
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
|
||||||
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
|
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
|
||||||
|
|
||||||
@@ -488,6 +492,47 @@ fun SettingsDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startupBehavior.equals("AUTO_CONNECT", ignoreCase = true) && savedHosts.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text("Auto-Connect Target Profile", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
var hostDropdownExpanded by remember { mutableStateOf(false) }
|
||||||
|
val selectedHost = savedHosts.find { it.id == selectedAutoHostId } ?: savedHosts.first()
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { hostDropdownExpanded = true },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${selectedHost.name} (${selectedHost.host}:${selectedHost.port})",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = hostDropdownExpanded,
|
||||||
|
onDismissRequest = { hostDropdownExpanded = false }
|
||||||
|
) {
|
||||||
|
savedHosts.forEach { host ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = "${host.name} (${host.host}:${host.port})",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = if (host.id == selectedAutoHostId) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
selectedAutoHostId = host.id
|
||||||
|
hostDropdownExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -914,6 +959,18 @@ fun SettingsDialog(
|
|||||||
numericFieldLock,
|
numericFieldLock,
|
||||||
autoSkipEnabled
|
autoSkipEnabled
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (startupBehavior.equals("AUTO_CONNECT", ignoreCase = true)) {
|
||||||
|
val targetHost = savedHosts.find { it.id == selectedAutoHostId }
|
||||||
|
if (targetHost != null) {
|
||||||
|
haus.nightmare.a3270.storage.HostStorage.saveHost(context, targetHost.copy(autoConnect = true))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val autoHost = haus.nightmare.a3270.storage.HostStorage.getAutoConnectHost(context)
|
||||||
|
if (autoHost != null) {
|
||||||
|
haus.nightmare.a3270.storage.HostStorage.saveHost(context, autoHost.copy(autoConnect = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ class TerminalInputView(context: Context) : View(context) {
|
|||||||
var onBackspace: () -> Unit = {}
|
var onBackspace: () -> Unit = {}
|
||||||
var onTab: () -> Unit = {}
|
var onTab: () -> Unit = {}
|
||||||
var onBackTab: () -> Unit = {}
|
var onBackTab: () -> Unit = {}
|
||||||
|
var isDbcsSession: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
imm?.restartInput(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isFocusable = true
|
isFocusable = true
|
||||||
@@ -97,9 +105,13 @@ class TerminalInputView(context: Context) : View(context) {
|
|||||||
override fun onCheckIsTextEditor(): Boolean = true
|
override fun onCheckIsTextEditor(): Boolean = true
|
||||||
|
|
||||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
|
||||||
outAttrs.inputType = InputType.TYPE_CLASS_TEXT or
|
outAttrs.inputType = if (isDbcsSession) {
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or
|
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||||
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
} else {
|
||||||
|
InputType.TYPE_CLASS_TEXT or
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or
|
||||||
|
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||||
|
}
|
||||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN or
|
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN or
|
||||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
||||||
EditorInfo.IME_ACTION_NONE
|
EditorInfo.IME_ACTION_NONE
|
||||||
|
|||||||
@@ -355,9 +355,9 @@ fun TerminalView(
|
|||||||
var csVal = 0
|
var csVal = 0
|
||||||
var ecVal = 0
|
var ecVal = 0
|
||||||
|
|
||||||
if (buf != null) {
|
val ea = if (buf != null) buf.getCell(addr) else null
|
||||||
val ea = buf.getCell(addr)
|
|
||||||
|
|
||||||
|
if (ea != null) {
|
||||||
if (ea.isFieldAttribute) {
|
if (ea.isFieldAttribute) {
|
||||||
currentFA = ea.fa
|
currentFA = ea.fa
|
||||||
currentFieldEa = ea
|
currentFieldEa = ea
|
||||||
@@ -411,7 +411,10 @@ fun TerminalView(
|
|||||||
bgColor = tmp
|
bgColor = tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bgColor != COLOR_BLACK && bgColor != Color.Transparent) {
|
val tr = if (ea != null && ea.tr != 0.toByte()) ea.tr else (currentFieldEa?.tr ?: 0)
|
||||||
|
val isOpaque = ((tr.toInt() and 0xFF) == (ExtendedAttribute.TR_OPAQUE.toInt() and 0xFF))
|
||||||
|
|
||||||
|
if (isOpaque || (bgColor != COLOR_BLACK && bgColor != Color.Transparent)) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = bgColor,
|
color = bgColor,
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
@@ -449,19 +452,22 @@ fun TerminalView(
|
|||||||
|
|
||||||
// Cursor indicator (respects blinking toggle & timer, cursor style, insert mode)
|
// Cursor indicator (respects blinking toggle & timer, cursor style, insert mode)
|
||||||
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
||||||
|
val isDbcsCur = ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT)
|
||||||
|
val curWidth = if (isDbcsCur) cellWidth * 2f else cellWidth
|
||||||
|
val curLeft = if (isDbcsCur && ea?.db == ExtendedAttribute.DB_RIGHT && c > 0) left - cellWidth else left
|
||||||
val isUnderlineCursor = cursorStyle.equals("UNDERLINE", ignoreCase = true) || isInsertMode
|
val isUnderlineCursor = cursorStyle.equals("UNDERLINE", ignoreCase = true) || isInsertMode
|
||||||
if (isUnderlineCursor) {
|
if (isUnderlineCursor) {
|
||||||
val ulH = maxOf(4f, cellHeight / 4f)
|
val ulH = maxOf(4f, cellHeight / 4f)
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color(255, 255, 255, 180),
|
color = Color(255, 255, 255, 180),
|
||||||
topLeft = Offset(left, top + cellHeight - ulH),
|
topLeft = Offset(curLeft, top + cellHeight - ulH),
|
||||||
size = Size(cellWidth, ulH)
|
size = Size(curWidth, ulH)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color(255, 255, 255, 180),
|
color = Color(255, 255, 255, 180),
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(curLeft, top),
|
||||||
size = Size(cellWidth, cellHeight)
|
size = Size(curWidth, cellHeight)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,12 +493,14 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val suppressText = isBlink && !textBlinkVisible
|
val isDbcsRight = ea?.db == ExtendedAttribute.DB_RIGHT
|
||||||
|
val isDbcsSoSi = ea?.db == ExtendedAttribute.DB_SO || ea?.db == ExtendedAttribute.DB_SI
|
||||||
|
val suppressText = (isBlink && !textBlinkVisible) || isDbcsRight || isDbcsSoSi
|
||||||
if (!drawnAsPs && charVal != ' ' && !suppressText) {
|
if (!drawnAsPs && charVal != ' ' && !suppressText) {
|
||||||
paint.color = fgColor.toArgb()
|
paint.color = fgColor.toArgb()
|
||||||
paint.isFakeBoldText = isBold
|
paint.isFakeBoldText = isBold
|
||||||
val fontMetrics = paint.fontMetrics
|
val fontMetrics = paint.fontMetrics
|
||||||
val textX = left + cellWidth / 2f
|
val textX = if (ea?.db == ExtendedAttribute.DB_LEFT) left + cellWidth else left + cellWidth / 2f
|
||||||
val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2f - fontMetrics.top
|
val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2f - fontMetrics.top
|
||||||
|
|
||||||
drawContext.canvas.nativeCanvas.drawText(
|
drawContext.canvas.nativeCanvas.drawText(
|
||||||
@@ -840,8 +848,8 @@ fun calculateGridMetrics(
|
|||||||
|
|
||||||
val gridW = cols * cellW
|
val gridW = cols * cellW
|
||||||
val gridH = rows * cellH
|
val gridH = rows * cellH
|
||||||
val offX = (viewWidth - gridW) / 2f
|
val offX = maxOf(0f, (viewWidth - gridW) / 2f)
|
||||||
val offY = (viewHeight - gridH) / 2f
|
val offY = maxOf(0f, (viewHeight - gridH) / 2f)
|
||||||
|
|
||||||
return TerminalGridMetrics(cellW, cellH, gridW, gridH, offX, offY)
|
return TerminalGridMetrics(cellW, cellH, gridW, gridH, offX, offY)
|
||||||
}
|
}
|
||||||
@@ -882,8 +890,21 @@ private fun copySelection(
|
|||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
if (addr < (rows * cols)) {
|
if (addr < (rows * cols)) {
|
||||||
val cell = screenBuffer.getCell(addr)
|
val cell = screenBuffer.getCell(addr)
|
||||||
if (cell != null && !cell.isFieldAttribute && cell.ucs4 > ' ' && cell.ucs4.code != 0xFFFF) {
|
if (cell != null && !cell.isFieldAttribute) {
|
||||||
line.append(cell.ucs4)
|
if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
|
val isSubsequent = if (blockSelectMode) c > minCol else (addr > (minRow * cols + minCol))
|
||||||
|
if (isSubsequent) {
|
||||||
|
continue // Character already added by DB_LEFT
|
||||||
|
} else {
|
||||||
|
line.append(' ')
|
||||||
|
}
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_SO || cell.db == ExtendedAttribute.DB_SI) {
|
||||||
|
line.append(' ')
|
||||||
|
} else if (cell.ucs4 > ' ' && cell.ucs4.code != 0xFFFF) {
|
||||||
|
line.append(cell.ucs4)
|
||||||
|
} else {
|
||||||
|
line.append(' ')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
line.append(' ')
|
line.append(' ')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.a3270.storage.HostStorage
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.CodePageRegistry
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
class DBCSAndParityCompatibilityTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNewJapaneseCodePagesAvailableAndResolvable() {
|
||||||
|
val availableKeys = HostStorage.AVAILABLE_CODE_PAGES.map { it.first }
|
||||||
|
assertTrue("290 should be available in HostStorage", availableKeys.contains("290"))
|
||||||
|
assertTrue("1027 should be available in HostStorage", availableKeys.contains("1027"))
|
||||||
|
assertTrue("1390 should be available in HostStorage", availableKeys.contains("1390"))
|
||||||
|
assertTrue("1399 should be available in HostStorage", availableKeys.contains("1399"))
|
||||||
|
|
||||||
|
val cp290 = CodePageRegistry.getCodePage("290")
|
||||||
|
assertNotNull("Cp290 must resolve", cp290)
|
||||||
|
assertFalse("Cp290 is SBCS", cp290.isDBCS)
|
||||||
|
|
||||||
|
val cp1027 = CodePageRegistry.getCodePage("1027")
|
||||||
|
assertNotNull("Cp1027 must resolve", cp1027)
|
||||||
|
assertFalse("Cp1027 is SBCS", cp1027.isDBCS)
|
||||||
|
|
||||||
|
val cp1390 = CodePageRegistry.getCodePage("1390")
|
||||||
|
assertNotNull("Cp1390 must resolve", cp1390)
|
||||||
|
assertTrue("Cp1390 is DBCS", cp1390.isDBCS)
|
||||||
|
|
||||||
|
val cp1399 = CodePageRegistry.getCodePage("1399")
|
||||||
|
assertNotNull("Cp1399 must resolve", cp1399)
|
||||||
|
assertTrue("Cp1399 is DBCS", cp1399.isDBCS)
|
||||||
|
|
||||||
|
val trans1390 = EbcdicTranslator("1390")
|
||||||
|
assertTrue("Translator for 1390 must report isDBCS", trans1390.isDBCS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testScreenExporterDBCSNoDuplication() {
|
||||||
|
val trans = EbcdicTranslator("930")
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, trans)
|
||||||
|
screen.erase(false)
|
||||||
|
|
||||||
|
// Put a 2-cell DBCS character at cells 0 and 1: '東' (\u6771)
|
||||||
|
val cell0 = screen.getCell(0)
|
||||||
|
cell0.db = ExtendedAttribute.DB_LEFT
|
||||||
|
cell0.ucs4 = '\u6771'
|
||||||
|
|
||||||
|
val cell1 = screen.getCell(1)
|
||||||
|
cell1.db = ExtendedAttribute.DB_RIGHT
|
||||||
|
cell1.ucs4 = '\u6771'
|
||||||
|
|
||||||
|
// Put SBCS 'X' at cell 2
|
||||||
|
val cell2 = screen.getCell(2)
|
||||||
|
cell2.db = ExtendedAttribute.DB_NONE
|
||||||
|
cell2.ucs4 = 'X'
|
||||||
|
|
||||||
|
// Invoke private generatePlainText via reflection
|
||||||
|
val exporterClass = Class.forName("haus.nightmare.a3270.ui.ScreenExporterDialogKt")
|
||||||
|
val method: Method = exporterClass.getDeclaredMethod(
|
||||||
|
"generatePlainText",
|
||||||
|
ScreenBuffer::class.java,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Boolean::class.javaPrimitiveType
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
|
||||||
|
val plainText = method.invoke(null, screen, 1, 80, true) as String
|
||||||
|
assertTrue("Output should start with single DBCS char + X: expected '東X', got: '$plainText'", plainText.startsWith("東X"))
|
||||||
|
assertFalse("DBCS char must not be doubled into '東東X'", plainText.startsWith("東東"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTerminalGridMetricsCenteringClampedAtZero() {
|
||||||
|
val terminalViewClass = Class.forName("haus.nightmare.a3270.ui.TerminalViewKt")
|
||||||
|
val method = terminalViewClass.getDeclaredMethod(
|
||||||
|
"calculateGridMetrics",
|
||||||
|
Float::class.javaPrimitiveType,
|
||||||
|
Float::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
|
||||||
|
// View is constrained / non-negative offsets
|
||||||
|
val constrainedMetrics = method.invoke(null, 400f, 300f, 80, 24)
|
||||||
|
val offXField = constrainedMetrics.javaClass.getDeclaredField("offsetX")
|
||||||
|
offXField.isAccessible = true
|
||||||
|
val offX = offXField.getFloat(constrainedMetrics)
|
||||||
|
|
||||||
|
val offYField = constrainedMetrics.javaClass.getDeclaredField("offsetY")
|
||||||
|
offYField.isAccessible = true
|
||||||
|
val offY = offYField.getFloat(constrainedMetrics)
|
||||||
|
|
||||||
|
assertTrue("Constrained offX must be non-negative", offX >= 0f)
|
||||||
|
assertTrue("Constrained offY must be non-negative", offY >= 0f)
|
||||||
|
|
||||||
|
// Wide view (pillarbox with positive offX)
|
||||||
|
val wideMetrics = method.invoke(null, 3000f, 1000f, 80, 24)
|
||||||
|
val wideOffX = offXField.getFloat(wideMetrics)
|
||||||
|
val wideOffY = offYField.getFloat(wideMetrics)
|
||||||
|
assertTrue("Wide view offX should be positive", wideOffX > 0f)
|
||||||
|
assertEquals(0f, wideOffY, 0.001f)
|
||||||
|
|
||||||
|
// Tall view (letterbox with positive offY)
|
||||||
|
val tallMetrics = method.invoke(null, 1000f, 3000f, 80, 24)
|
||||||
|
val tallOffX = offXField.getFloat(tallMetrics)
|
||||||
|
val tallOffY = offYField.getFloat(tallMetrics)
|
||||||
|
assertEquals(0f, tallOffX, 0.001f)
|
||||||
|
assertTrue("Tall view offY should be positive", tallOffY > 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExtendedAttributeTransparencyConstants() {
|
||||||
|
assertEquals(0x00.toByte(), ExtendedAttribute.TR_DEFAULT)
|
||||||
|
assertEquals(0xF0.toByte(), ExtendedAttribute.TR_OR)
|
||||||
|
assertEquals(0xFF.toByte(), ExtendedAttribute.TR_OPAQUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user