From ee4475966809697d267c59ef745234853443b86f Mon Sep 17 00:00:00 2001 From: Rudi Date: Tue, 22 Sep 2026 13:41:10 +0000 Subject: [PATCH] Update Status Bar, Settings, Terminal alignment and codepage testing --- .../java/haus/nightmare/a3270/MainActivity.kt | 36 ++++- .../nightmare/a3270/storage/HostStorage.kt | 4 + .../haus/nightmare/a3270/ui/OiaStatusBar.kt | 15 ++- .../a3270/ui/ScreenExporterDialog.kt | 9 ++ .../haus/nightmare/a3270/ui/SettingsDialog.kt | 57 ++++++++ .../nightmare/a3270/ui/TerminalInputView.kt | 18 ++- .../haus/nightmare/a3270/ui/TerminalView.kt | 47 +++++-- .../a3270/DBCSAndParityCompatibilityTest.kt | 125 ++++++++++++++++++ 8 files changed, 286 insertions(+), 25 deletions(-) create mode 100644 src/test/java/haus/nightmare/a3270/DBCSAndParityCompatibilityTest.kt diff --git a/src/main/java/haus/nightmare/a3270/MainActivity.kt b/src/main/java/haus/nightmare/a3270/MainActivity.kt index ce1ff85..ab75fed 100644 --- a/src/main/java/haus/nightmare/a3270/MainActivity.kt +++ b/src/main/java/haus/nightmare/a3270/MainActivity.kt @@ -546,11 +546,34 @@ fun MainScreen( val hotspotsPfEnabled by viewModel.hotspotsPfEnabled.collectAsState() val hotspotsMenuChoicesEnabled by viewModel.hotspotsMenuChoicesEnabled.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 isDbcsSession = remember(screenBuffer, screenVersion) { + screenBuffer?.translator?.isDBCS == true + } + + 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) } @@ -717,6 +740,7 @@ fun MainScreen( AndroidView( factory = { ctx -> TerminalInputView(ctx).apply { + this.isDbcsSession = isDbcsSession onInputText = { text -> viewModel.typeString(text) onClearShift() @@ -738,6 +762,7 @@ fun MainScreen( } }, update = { inputView -> + inputView.isDbcsSession = isDbcsSession terminalInputViewRef = inputView }, modifier = Modifier.size(1.dp) @@ -880,6 +905,7 @@ fun MainScreen( inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0, luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "", isNumericField = isNumericField, + fieldStatus = fieldStatus, uiTheme = uiTheme, onToggleDocMode = { viewModel.toggleDocMode() }, onToggleWordWrap = { viewModel.toggleWordWrap() }, diff --git a/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt b/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt index b9d93fd..ca5b7b7 100644 --- a/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt +++ b/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt @@ -176,6 +176,10 @@ object HostStorage { "1149" to "1149 - Iceland (Euro €)", "930" to "930 - Japanese Katakana 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", "937" to "937 - Traditional Chinese Mixed DBCS", "1388" to "1388 - Simplified Chinese Extended DBCS", diff --git a/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt b/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt index f87d400..5e9d0a6 100644 --- a/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt +++ b/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt @@ -40,6 +40,7 @@ fun OiaStatusBar( inhibitReason: Int = 0, luName: String = "", isNumericField: Boolean = false, + fieldStatus: String = "", uiTheme: String = "DARK", 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) { + 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)) Surface( - color = Color(0xFF339AF0).copy(alpha = 0.2f), + color = badgeColor.copy(alpha = 0.2f), shape = RoundedCornerShape(4.dp) ) { Text( - text = if (isNumericField) "NUM" else "ALPHA", - color = if (isNumericField) Color(0xFFFFD43B) else Color(0xFF74C0FC), + text = effectiveFieldStatus, + color = badgeColor, fontSize = 9.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, diff --git a/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt b/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt index 3b4671f..79b3c45 100644 --- a/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt @@ -33,6 +33,7 @@ import androidx.core.content.FileProvider import haus.nightmare.lib3270j.graphics.GraphicsPlane import haus.nightmare.lib3270j.graphics.ProgramSymbolManager import haus.nightmare.lib3270j.protocol.DS3270Constants.* +import haus.nightmare.lib3270j.screen.ExtendedAttribute import haus.nightmare.lib3270j.screen.ScreenBuffer import java.io.File import java.io.FileOutputStream @@ -296,6 +297,10 @@ private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, val faVal = cell.fa.toInt() and 0xFF currentHidden = (faVal and FA_INT_HIGH_SEL) == 0 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 { if (currentHidden && maskHidden) { rowSb.append(' ') @@ -346,6 +351,10 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask currentHidden = (faVal and FA_INT_HIGH_SEL) == 0 currentFieldEa = cell 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 { if (currentHidden && maskHidden) { sb.append(' ') diff --git a/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt b/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt index 367081e..5fc69ed 100644 --- a/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt @@ -111,6 +111,10 @@ fun SettingsDialog( var hotspotsPfEnabled by remember { mutableStateOf(initialHotspotsPfEnabled) } var hotspotsMenuChoicesEnabled by remember { mutableStateOf(initialHotspotsMenuChoicesEnabled) } 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 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)) @@ -914,6 +959,18 @@ fun SettingsDialog( numericFieldLock, 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)) ) { diff --git a/src/main/java/haus/nightmare/a3270/ui/TerminalInputView.kt b/src/main/java/haus/nightmare/a3270/ui/TerminalInputView.kt index 37f42a8..32ef3e0 100644 --- a/src/main/java/haus/nightmare/a3270/ui/TerminalInputView.kt +++ b/src/main/java/haus/nightmare/a3270/ui/TerminalInputView.kt @@ -88,6 +88,14 @@ class TerminalInputView(context: Context) : View(context) { var onBackspace: () -> Unit = {} var onTab: () -> 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 { isFocusable = true @@ -97,9 +105,13 @@ class TerminalInputView(context: Context) : View(context) { override fun onCheckIsTextEditor(): Boolean = true override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection { - outAttrs.inputType = InputType.TYPE_CLASS_TEXT or - InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + outAttrs.inputType = if (isDbcsSession) { + InputType.TYPE_CLASS_TEXT or 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 EditorInfo.IME_FLAG_NO_EXTRACT_UI or EditorInfo.IME_ACTION_NONE diff --git a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt index 46b68aa..2732d30 100644 --- a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt +++ b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt @@ -355,9 +355,9 @@ fun TerminalView( var csVal = 0 var ecVal = 0 - if (buf != null) { - val ea = buf.getCell(addr) + val ea = if (buf != null) buf.getCell(addr) else null + if (ea != null) { if (ea.isFieldAttribute) { currentFA = ea.fa currentFieldEa = ea @@ -411,7 +411,10 @@ fun TerminalView( 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( color = bgColor, topLeft = Offset(left, top), @@ -449,19 +452,22 @@ fun TerminalView( // Cursor indicator (respects blinking toggle & timer, cursor style, insert mode) 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 if (isUnderlineCursor) { val ulH = maxOf(4f, cellHeight / 4f) drawRect( color = Color(255, 255, 255, 180), - topLeft = Offset(left, top + cellHeight - ulH), - size = Size(cellWidth, ulH) + topLeft = Offset(curLeft, top + cellHeight - ulH), + size = Size(curWidth, ulH) ) } else { drawRect( color = Color(255, 255, 255, 180), - topLeft = Offset(left, top), - size = Size(cellWidth, cellHeight) + topLeft = Offset(curLeft, top), + 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) { paint.color = fgColor.toArgb() paint.isFakeBoldText = isBold 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 drawContext.canvas.nativeCanvas.drawText( @@ -840,8 +848,8 @@ fun calculateGridMetrics( val gridW = cols * cellW val gridH = rows * cellH - val offX = (viewWidth - gridW) / 2f - val offY = (viewHeight - gridH) / 2f + val offX = maxOf(0f, (viewWidth - gridW) / 2f) + val offY = maxOf(0f, (viewHeight - gridH) / 2f) return TerminalGridMetrics(cellW, cellH, gridW, gridH, offX, offY) } @@ -882,8 +890,21 @@ private fun copySelection( val addr = r * cols + c if (addr < (rows * cols)) { val cell = screenBuffer.getCell(addr) - if (cell != null && !cell.isFieldAttribute && cell.ucs4 > ' ' && cell.ucs4.code != 0xFFFF) { - line.append(cell.ucs4) + if (cell != null && !cell.isFieldAttribute) { + 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 { line.append(' ') } diff --git a/src/test/java/haus/nightmare/a3270/DBCSAndParityCompatibilityTest.kt b/src/test/java/haus/nightmare/a3270/DBCSAndParityCompatibilityTest.kt new file mode 100644 index 0000000..5472c0b --- /dev/null +++ b/src/test/java/haus/nightmare/a3270/DBCSAndParityCompatibilityTest.kt @@ -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) + } +}