Codepage, ECL, and some UI updates
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m29s

This commit is contained in:
2026-09-22 13:38:12 +00:00
parent d05f1a4c45
commit 3d3a939ece
29 changed files with 1773 additions and 92 deletions
@@ -357,9 +357,12 @@ public class StatusBar extends JPanel {
wordWrapStatus.setText("");
}
// Field status: Numeric vs Alphanumeric
// Field status: Numeric vs Alphanumeric vs DBCS
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) {
if (client.getOIA().isDBCS()) {
fieldTypeStatus.setText("DBCS");
fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else {
@@ -10,6 +10,12 @@ import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.awt.event.InputMethodEvent;
import java.awt.event.InputMethodListener;
import java.awt.font.TextHitInfo;
import java.awt.im.InputMethodRequests;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
@@ -18,7 +24,8 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
* Supports colors, bold, underline, reverse, blink, GOCA vector graphics,
* programmed symbols, search highlighting, and all 3278/3279 attributes.
*/
public class TerminalPanel extends JPanel implements java.awt.print.Printable {
public class TerminalPanel extends JPanel implements java.awt.print.Printable, InputMethodListener, InputMethodRequests {
private Telnet3270Client client;
@@ -216,6 +223,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
enableInputMethods(true);
addInputMethodListener(this);
// Handle mouse clicks to position cursor, selection, and grab focus
MouseAdapter mouseHandler = new MouseAdapter() {
@Override
@@ -715,6 +726,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
repaint();
}
public void select(int startRow, int startCol, int endRow, int endCol) {
selectionStartRow = startRow;
selectionStartCol = startCol;
selectionEndRow = endRow;
selectionEndCol = endCol;
repaint();
}
public boolean hasSelection() {
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
@@ -740,11 +759,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
for (int c = c1; c <= c2; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getCell(baddr);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
if (ea.isFieldAttribute()) {
line.append(' ');
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
if (c > c1) {
continue; // Character already added by DB_LEFT
} else {
line.append(' ');
}
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
line.append(' ');
} else {
line.append(ch);
char ch = ea.ucs4;
if (ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
}
}
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
@@ -782,11 +813,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
prevRow = curRow;
}
ExtendedAttribute ea = sb.getCell(addr);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
if (ea.isFieldAttribute()) {
line.append(' ');
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
if (addr > startAddr) {
continue; // Character already added by DB_LEFT
} else {
line.append(' ');
}
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
line.append(' ');
} else {
line.append(ch);
char ch = ea.ucs4;
if (ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
}
}
int end = line.length();
@@ -2099,13 +2142,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
continue;
}
byte tr = ea.tr != 0 ? ea.tr : (currentFieldEa != null ? currentFieldEa.tr : 0);
boolean isOpaque = ((tr & 0xFF) == XAT_OPAQUE);
if (reverse) {
Color tmp = fgColor;
fgColor = (rawBg == 0) ? this.bgColor : bgColor;
bgColor = tmp;
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
} else if (bgIsExplicit || !bgColor.equals(this.bgColor)) {
} else if (isOpaque || bgIsExplicit || !bgColor.equals(this.bgColor)) {
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
}
@@ -2120,7 +2166,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (cs >= 0x40 && client.getProgramSymbolManager() != null && !suppressGlyph) {
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) {
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
int symBg = (bgIsExplicit || reverse || isOpaque) ? bgColor.getRGB() : 0;
haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg);
java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb);
if (img != null) {
@@ -2130,7 +2176,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
if (!drawnAsPs && !suppressGlyph) {
if (!drawnAsPs && !suppressGlyph && ea.db != ExtendedAttribute.DB_RIGHT
&& ea.db != ExtendedAttribute.DB_SO && ea.db != ExtendedAttribute.DB_SI) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? boldTerminalFont : terminalFont;
@@ -2269,11 +2316,17 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
boolean isUnderline = (cursorStyle == CursorStyle.UNDERLINE) ||
(client.getInputProcessor() != null && client.getInputProcessor().isInsertMode());
g2.setColor(CURSOR_COLOR);
ExtendedAttribute curEa = (curAddr >= 0 && curAddr < sb.getSize()) ? sb.getDisplayCell(curAddr) : null;
boolean isDbcs = curEa != null && (curEa.db == ExtendedAttribute.DB_LEFT || curEa.db == ExtendedAttribute.DB_RIGHT);
int cWidth = isDbcs ? (cellWidth * 2) : cellWidth;
if (isDbcs && curEa.db == ExtendedAttribute.DB_RIGHT && curCol > 0) {
cx -= cellWidth;
}
if (isUnderline) {
int ulH = Math.max(2, cellHeight / 6);
g2.fillRect(cx, cy + cellHeight - ulH, cellWidth, ulH);
g2.fillRect(cx, cy + cellHeight - ulH, cWidth, ulH);
} else {
g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.fillRect(cx, cy, cWidth, cellHeight);
}
}
}
@@ -2462,4 +2515,90 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
paint(g2d);
return PAGE_EXISTS;
}
// =========================================================================
// Java Input Method Framework (IMF / IME) Support
// =========================================================================
@Override
public InputMethodRequests getInputMethodRequests() {
return this;
}
@Override
public void inputMethodTextChanged(InputMethodEvent event) {
if (client == null || !client.getConnectionState().isFullSession()) {
event.consume();
return;
}
AttributedCharacterIterator text = event.getText();
if (text != null) {
int committedCount = event.getCommittedCharacterCount();
char c = text.first();
for (int i = 0; i < committedCount && c != CharacterIterator.DONE; i++) {
client.typeCharacter(c);
c = text.next();
}
refreshScreen();
}
event.consume();
}
@Override
public void caretPositionChanged(InputMethodEvent event) {
// No-op
}
@Override
public Rectangle getTextLocation(TextHitInfo offset) {
Point p;
try {
p = getLocationOnScreen();
} catch (IllegalComponentStateException e) {
p = new Point(0, 0);
}
int curRow = 0, curCol = 0;
if (client != null && client.getScreenBuffer() != null) {
int cols = client.getScreenBuffer().getCols();
int curAddr = client.getScreenBuffer().getDisplayCursorAddress();
if (cols > 0) {
curRow = curAddr / cols;
curCol = curAddr % cols;
}
}
int cx = p.x + getRenderOffsetX() + curCol * cellWidth;
int cy = p.y + getRenderOffsetY() + curRow * cellHeight;
return new Rectangle(cx, cy, cellWidth, cellHeight);
}
@Override
public TextHitInfo getLocationOffset(int x, int y) {
return TextHitInfo.leading(0);
}
@Override
public int getInsertPositionOffset() {
return 0;
}
@Override
public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public int getCommittedTextLength() {
return 0;
}
@Override
public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
}
@@ -0,0 +1,242 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AutoConnectSettingsAndTerminalCenteringTest {
private List<SavedHost> originalSavedHosts;
private Settings.StartupBehavior originalStartupBehavior;
private String originalAutoConnectHost;
private int originalAutoConnectPort;
@BeforeEach
public void setUp() {
originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts());
originalStartupBehavior = Settings.getStartupBehavior();
originalAutoConnectHost = Settings.getAutoConnectHost();
originalAutoConnectPort = Settings.getAutoConnectPort();
}
@AfterEach
public void tearDown() {
HostStorage.saveAll(originalSavedHosts);
Settings.setStartupBehavior(originalStartupBehavior);
Settings.setAutoConnectHost(originalAutoConnectHost);
Settings.setAutoConnectPort(originalAutoConnectPort);
}
@Test
@DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big")
public void testTerminalPanelCenteringLogic() {
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
panel.setClient(client);
int cols = sb.getDisplayCols(); // 80
int rows = sb.getDisplayRows(); // 24
int gridW = cols * panel.cellWidth; // 800
int gridH = rows * panel.cellHeight; // 480
// 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0
panel.setSize(gridW - 100, gridH - 50);
assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained");
assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained");
// 2. Panel exactly matches grid: offset must be 0
panel.setSize(gridW, gridH);
assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid");
assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid");
// 3. Panel is larger with odd extra (extra = 7)
panel.setSize(gridW + 7, gridH + 11);
int ox = panel.getRenderOffsetX();
int leftSpace = ox;
int rightSpace = (gridW + 7) - (ox + gridW);
assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")");
assertEquals(3, leftSpace);
assertEquals(4, rightSpace);
int oy = panel.getRenderOffsetY();
int topSpace = oy;
int bottomSpace = (gridH + 11) - (oy + gridH);
assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space");
assertEquals(5, topSpace);
assertEquals(6, bottomSpace);
// 4. Panel is larger with even extra (extra = 6)
panel.setSize(gridW + 6, gridH + 8);
assertEquals(3, panel.getRenderOffsetX());
assertEquals(4, panel.getRenderOffsetY());
panel.dispose();
}
@Test
@DisplayName("guardedPack does not resize window when window is too small")
public void testGuardedPackDoesNotResizeWhenTooSmall() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
frame.getContentPane().add(panel);
frame.setSize(400, 300);
frame.addNotify(); // makes frame displayable
// Set size explicitly smaller than preferred size (800+ x 480+)
frame.setSize(400, 300);
Dimension sizeBefore = frame.getSize();
// Run guardedPack
panel.guardedPack();
// Window should not have expanded
Dimension sizeAfter = frame.getSize();
assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small");
assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small");
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {
// Environment without X display
}
}
@Test
@DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host")
public void testSettingsDialogSavedConnectionsDropdown() throws Exception {
SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", "");
List<SavedHost> testHosts = new ArrayList<>();
testHosts.add(host1);
testHosts.add(host2);
HostStorage.saveAll(testHosts);
Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT);
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JComboBox<SavedHost> combo = dialog.getSavedHostBox();
assertNotNull(combo, "savedHostBox should not be null");
assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts");
// Host Alpha was autoConnect=true, so it should be initially selected
SavedHost initialSel = (SavedHost) combo.getSelectedItem();
assertNotNull(initialSel);
assertEquals("id-test-1", initialSel.getId());
// Switch to Host Beta
combo.setSelectedIndex(1);
// Trigger applySettings via reflection
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
applyMethod.setAccessible(true);
applyMethod.invoke(dialog);
// Verify Host Beta is now the auto-connect host
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto, "Auto-connect host should exist");
assertEquals("id-test-2", auto.getId());
assertEquals("Host Beta", auto.getName());
assertEquals("beta.mainframe.test", auto.getHost());
assertEquals(992, auto.getPort());
SavedHost loadedAlpha = HostStorage.findById("id-test-1");
assertNotNull(loadedAlpha);
assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true");
// Verify Settings autoConnectHost/Port synchronized
assertEquals("beta.mainframe.test", Settings.getAutoConnectHost());
assertEquals(992, Settings.getAutoConnectPort());
// Switch startup behavior to SHOW_CONNECT and apply
Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox");
sbField.setAccessible(true);
@SuppressWarnings("unchecked")
JComboBox<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) sbField.get(dialog);
sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT);
applyMethod.invoke(dialog);
assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared");
dialog.dispose();
app.dispose();
} finally {
HostStorage.saveAll(originalSavedHosts);
}
}
@Test
@DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged")
public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
frame.getContentPane().add(panel);
frame.addNotify();
// Pack for Model 4 (43 rows, 80 cols)
panel.packForModel(43, 80);
Dimension model4Size = frame.getSize();
assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows");
assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols");
// Pack for Model 5 (27 rows, 132 cols)
panel.packForModel(27, 132);
Dimension model5Size = frame.getSize();
assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols");
// Set custom size and verify guardedPack leaves the window alone
frame.setSize(600, 400);
Dimension fixedSize = frame.getSize();
panel.guardedPack();
assertEquals(fixedSize.width, frame.getWidth());
assertEquals(fixedSize.height, frame.getHeight());
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {}
}
}
@@ -0,0 +1,153 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.event.InputMethodEvent;
import java.text.AttributedString;
import static org.junit.jupiter.api.Assertions.*;
public class DBCSInputAndClipboardTest {
@Test
@DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSClipboardCopyNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'A';
panel.setClient(client);
// Stream selection covering cells 0..2 (row 0, col 0..2)
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character");
}
@Test
@DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSBlockSelectionNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.setBlockSelectMode(true);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'B';
panel.setClient(client);
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character");
}
@Test
@DisplayName("InputMethodRequests returns cursor bounds and processes committed text")
public void testDBCSInputMethodRequestsAndEvents() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this");
Rectangle loc = panel.getTextLocation(null);
assertNotNull(loc, "Text location must not be null");
assertTrue(loc.width > 0, "Cursor width must be positive");
assertTrue(loc.height > 0, "Cursor height must be positive");
// Simulate committed IME text '\u3000'
AttributedString as = new AttributedString("\u3000");
InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
as.getIterator(), 1, null, null);
panel.inputMethodTextChanged(imeEvent);
ScreenBuffer sb = client.getScreenBuffer();
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals('\u3000', sb.getCell(0).ucs4);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
assertEquals('\u3000', sb.getCell(1).ucs4);
assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS");
}
@Test
@DisplayName("InputProcessor backspace over DBCS character deletes both cells")
public void testInputProcessorDBCSTypingAndBackspace() {
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
InputProcessor input = client.getInputProcessor();
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
sb.setCursorAddress(0);
// Type DBCS character
input.typeCharacter('\u3000');
assertEquals(2, sb.getCursorAddress());
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
// Backspace over DBCS character
input.backspace();
assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells");
assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)");
assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)");
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db);
}
}