IBM-DYNAMIC
Release j3270 / Build & Publish Release (push) Successful in 1m23s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m28s

This commit is contained in:
2026-09-03 16:27:01 -04:00
parent db6043cb39
commit 6296cf4341
210 changed files with 16655 additions and 299 deletions
@@ -178,6 +178,38 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(themeMenu);
viewMenu.addSeparator();
JCheckBoxMenuItem rulerItem = new JCheckBoxMenuItem("Crosshair Ruler", terminalPanel.isCrosshairRulerEnabled());
ThemeManager.styleMenuItem(rulerItem);
rulerItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
rulerItem.addActionListener(e -> {
boolean en = rulerItem.isSelected();
terminalPanel.setCrosshairRulerEnabled(en);
haus.nightmare.j3270.config.Settings.setCrosshairRuler(en);
});
viewMenu.add(rulerItem);
JMenu cursorMenu = createMenu("Cursor Style");
ButtonGroup cursorGroup = new ButtonGroup();
JRadioButtonMenuItem blockCursorItem = new JRadioButtonMenuItem("Block Cursor", terminalPanel.getCursorStyle() == TerminalPanel.CursorStyle.BLOCK);
ThemeManager.styleMenuItem(blockCursorItem);
blockCursorItem.addActionListener(e -> {
terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.BLOCK);
haus.nightmare.j3270.config.Settings.setCursorStyle("BLOCK");
});
cursorGroup.add(blockCursorItem);
cursorMenu.add(blockCursorItem);
JRadioButtonMenuItem underlineCursorItem = new JRadioButtonMenuItem("Underline Cursor", terminalPanel.getCursorStyle() == TerminalPanel.CursorStyle.UNDERLINE);
ThemeManager.styleMenuItem(underlineCursorItem);
underlineCursorItem.addActionListener(e -> {
terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE);
haus.nightmare.j3270.config.Settings.setCursorStyle("UNDERLINE");
});
cursorGroup.add(underlineCursorItem);
cursorMenu.add(underlineCursorItem);
viewMenu.add(cursorMenu);
viewMenu.addSeparator();
// CodePage Submenu
JMenu cpMenu = createMenu("Code Page");
String[] codePages = {
@@ -704,7 +736,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"Delete — Delete character\n" +
"Backspace — Backspace\n" +
"Insert — Toggle insert mode\n" +
"Escape — Reset\n" +
"Escape / Alt+R — Reset\n" +
"PageUp/Down — PF7/PF8\n" +
"Alt+C / Alt+K — Clear\n" +
"Alt+E — Erase Input\n" +
@@ -713,6 +745,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"Alt+Q — Cursor Select\n" +
"Alt+L — Toggle Light Pen\n" +
"Alt+T — File Transfer\n" +
"Alt+Shift+R — Crosshair Ruler\n" +
"Alt+=/-/0 — Font size +/-/reset\n" +
"Cmd/Ctrl+F — Find on Screen\n" +
"Cmd/Ctrl+G / F3— Find Next\n" +
@@ -893,10 +926,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
TerminalModel model = TerminalModel.IBM_3279_4;
if (remainingArgs.size() >= 3) {
try {
int modelNum = Integer.parseInt(remainingArgs.get(2));
model = TerminalModel.forModel(modelNum, true);
} catch (Exception ignored) {}
String mArg = remainingArgs.get(2).trim();
if (mArg.equalsIgnoreCase("dynamic") || mArg.equals("0")) {
model = TerminalModel.IBM_DYNAMIC;
} else {
try {
int modelNum = Integer.parseInt(mArg);
model = TerminalModel.forModel(modelNum, true);
} catch (Exception ignored) {}
}
}
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
if (finalTls) {
@@ -113,6 +113,22 @@ public class Settings {
prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037");
}
public static int getDynamicRows() {
return prefs.getInt("dynamicRows", 62);
}
public static void setDynamicRows(int rows) {
prefs.putInt("dynamicRows", Math.max(1, rows));
}
public static int getDynamicCols() {
return prefs.getInt("dynamicCols", 160);
}
public static void setDynamicCols(int cols) {
prefs.putInt("dynamicCols", Math.max(1, cols));
}
public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null);
try {
@@ -233,6 +249,26 @@ public class Settings {
prefs.putBoolean("blockSelectMode", block);
}
// ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() {
return prefs.getBoolean("crosshairRuler", false);
}
public static void setCrosshairRuler(boolean enabled) {
prefs.putBoolean("crosshairRuler", enabled);
}
// ========== Cursor Style ==========
public static String getCursorStyle() {
return prefs.get("cursorStyle", "BLOCK");
}
public static void setCursorStyle(String style) {
prefs.put("cursorStyle", style != null ? style.toUpperCase() : "BLOCK");
}
private static void applyConfigEntry(String section, String key, String value) {
switch (section) {
case "appearance":
@@ -244,6 +280,13 @@ public class Settings {
case "uiTheme":
setJavaUiTheme(UITheme.fromString(value));
break;
case "crosshairRuler":
case "ruler":
setCrosshairRuler(Boolean.parseBoolean(value));
break;
case "cursorStyle":
setCursorStyle(value);
break;
default:
log.warning("Unknown appearance key: " + key);
}
@@ -284,6 +327,14 @@ public class Settings {
case "charset":
setCodePage(value);
break;
case "dynamicRows":
case "dynamic_rows":
setDynamicRows(Integer.parseInt(value));
break;
case "dynamicCols":
case "dynamic_cols":
setDynamicCols(Integer.parseInt(value));
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
default:
log.warning("Unknown behavior/connection key: " + key);
@@ -340,6 +391,8 @@ public class Settings {
w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize());
w.println("crosshairRuler = " + getCrosshairRuler());
w.println("cursorStyle = " + getCursorStyle());
w.println();
// [behavior]
@@ -354,6 +407,8 @@ public class Settings {
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
}
w.println("codePage = " + getCodePage());
w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode());
w.println();
@@ -14,6 +14,10 @@ public class ConnectDialog extends JDialog {
private JTextField hostField;
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
private JLabel dynamicDimLabel;
private JPanel dynamicDimPanel;
private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JComboBox<String> codePageCombo;
private JTextField luField;
@@ -79,10 +83,49 @@ public class ConnectDialog extends JDialog {
ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc);
// LU Name
// Dynamic Dimensions (shown when IBM-DYNAMIC is selected)
gbc.gridx = 0;
gbc.gridy = 3;
gbc.weightx = 0;
dynamicDimLabel = new JLabel("Screen Size:");
dynamicDimLabel.setFont(labelFont);
mainPanel.add(dynamicDimLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
dynamicDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
dynamicDimPanel.setOpaque(false);
JLabel rowsLabel = new JLabel("Rows:");
rowsLabel.setFont(labelFont);
dynamicRowsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicRows(), 24, 255, 1));
ThemeManager.styleSpinner(dynamicRowsSpinner);
JLabel colsLabel = new JLabel("Cols:");
colsLabel.setFont(labelFont);
dynamicColsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicCols(), 80, 255, 1));
ThemeManager.styleSpinner(dynamicColsSpinner);
dynamicDimPanel.add(rowsLabel);
dynamicDimPanel.add(dynamicRowsSpinner);
dynamicDimPanel.add(colsLabel);
dynamicDimPanel.add(dynamicColsSpinner);
mainPanel.add(dynamicDimPanel, gbc);
Runnable updateDynamicVisibility = () -> {
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
boolean isDyn = (m != null && m.isDynamic());
dynamicDimLabel.setVisible(isDyn);
dynamicDimPanel.setVisible(isDyn);
pack();
};
modelCombo.addActionListener(e -> updateDynamicVisibility.run());
updateDynamicVisibility.run();
// LU Name
gbc.gridx = 0;
gbc.gridy = 4;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
@@ -93,7 +136,7 @@ public class ConnectDialog extends JDialog {
// Graphics Mode
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridy = 5;
gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setFont(labelFont);
@@ -108,7 +151,7 @@ public class ConnectDialog extends JDialog {
// Code Page
gbc.gridx = 0;
gbc.gridy = 5;
gbc.gridy = 6;
gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setFont(labelFont);
@@ -167,7 +210,7 @@ public class ConnectDialog extends JDialog {
// TLS / SSL Checkbox
gbc.gridx = 1;
gbc.gridy = 6;
gbc.gridy = 7;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
ThemeManager.styleCheckBox(tlsCheckBox);
@@ -186,7 +229,7 @@ public class ConnectDialog extends JDialog {
// Verify Certificate Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
gbc.gridy = 8;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
@@ -196,7 +239,7 @@ public class ConnectDialog extends JDialog {
// TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 8;
gbc.gridy = 9;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
@@ -223,7 +266,7 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 9;
gbc.gridy = 10;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
@@ -256,7 +299,15 @@ public class ConnectDialog extends JDialog {
return;
}
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem());
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
result = new ConnectionConfig(host, port, selectedModel);
if (selectedModel != null && selectedModel.isDynamic()) {
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
result.setDynamicDimensions(dRows, dCols);
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
}
String lu = luField.getText().trim();
if (!lu.isEmpty()) {
result.setLuName(lu);
@@ -98,19 +98,21 @@ public class ScreenExporter {
writer.write("<style>\n");
writer.write("body { background-color: #0a0a0a; color: #32cd32; font-family: 'Courier New', Courier, monospace; font-size: 14px; margin: 20px; }\n");
writer.write(".screen { background-color: #000; padding: 15px; border-radius: 4px; display: inline-block; box-shadow: 0 0 10px rgba(0,0,0,0.8); line-height: 1.2; }\n");
writer.write(".c-blue { color: #5078ff; }\n");
writer.write(".c-red { color: #ff3232; }\n");
writer.write(".c-pink { color: #ff82b4; }\n");
writer.write(".c-green { color: #32cd32; }\n");
writer.write(".c-turq { color: #40e0d0; }\n");
writer.write(".c-yellow { color: #ffff50; }\n");
writer.write(".c-blue { color: #7890f0; }\n");
writer.write(".c-red { color: #ff0000; }\n");
writer.write(".c-pink { color: #ff00ff; }\n");
writer.write(".c-green { color: #00ff00; }\n");
writer.write(".c-turq { color: #00ffff; }\n");
writer.write(".c-yellow { color: #ffff00; }\n");
writer.write(".c-white { color: #ffffff; }\n");
writer.write(".c-black { color: #000000; }\n");
writer.write(".c-orange { color: #ffa500; }\n");
writer.write(".c-purple { color: #b482ff; }\n");
writer.write(".c-palegreen { color: #90ee90; }\n");
writer.write(".c-paleturq { color: #afeeee; }\n");
writer.write(".c-grey { color: #aaaaaa; }\n");
writer.write(".c-deepblue { color: #000080; }\n");
writer.write(".c-orange { color: #ffa200; }\n");
writer.write(".c-purple { color: #800080; }\n");
writer.write(".c-palegreen { color: #008000; }\n");
writer.write(".c-paleturq { color: #008080; }\n");
writer.write(".c-mustard { color: #a0a000; }\n");
writer.write(".c-grey { color: #c0c0c0; }\n");
writer.write(".bold { font-weight: bold; }\n");
writer.write(".underline { text-decoration: underline; }\n");
writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>");
@@ -209,17 +211,17 @@ public class ScreenExporter {
case 6: return "c-yellow";
case 7: return "c-white";
case 8: return "c-black";
case 9: return "c-blue";
case 9: return "c-deepblue";
case 10: return "c-orange";
case 11: return "c-purple";
case 12: return "c-palegreen";
case 13: return "c-paleturq";
case 14: return "c-grey";
case 15: return "c-white";
case 14: return "c-mustard";
case 15: return "c-grey";
}
}
if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue";
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-turq";
}
return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
}
@@ -31,6 +31,8 @@ public class SettingsDialog extends JDialog {
private JTextField hostField;
private JTextField portField;
private JCheckBox blockSelectCheck;
private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner;
// Advanced tab state tracking
private final Color[] tempHostColors = new Color[16];
@@ -235,8 +237,30 @@ public class SettingsDialog extends JDialog {
blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
panel.add(blockSelectCheck, gbc);
// Placeholder for potentially more behavior options below
// Default Dynamic Screen Size
gbc.gridy = 3;
gbc.gridwidth = 1;
gbc.gridx = 0;
panel.add(new JLabel("Default Dynamic Screen:"), gbc);
JPanel dynDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
dynDimPanel.setOpaque(false);
dynDimPanel.add(new JLabel("Rows:"));
dynamicRowsSpinner = new JSpinner(new SpinnerNumberModel(Settings.getDynamicRows(), 24, 255, 1));
ThemeManager.styleSpinner(dynamicRowsSpinner);
dynDimPanel.add(dynamicRowsSpinner);
dynDimPanel.add(new JLabel("Cols:"));
dynamicColsSpinner = new JSpinner(new SpinnerNumberModel(Settings.getDynamicCols(), 80, 255, 1));
ThemeManager.styleSpinner(dynamicColsSpinner);
dynDimPanel.add(dynamicColsSpinner);
gbc.gridx = 1;
panel.add(dynDimPanel, gbc);
// Placeholder for potentially more behavior options below
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc);
@@ -555,6 +579,12 @@ public class SettingsDialog extends JDialog {
// Block select mode
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
// Default Dynamic screen dimensions
if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
Settings.setDynamicCols((Integer) dynamicColsSpinner.getValue());
}
// Propagate visual changes to the app
// Save Colors
for (int i=0; i<16; i++) {
@@ -110,25 +110,25 @@ public class StatusBar extends JPanel {
break;
case CONNECTED_3270:
connectionStatus.setText("TN3270");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
break;
case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
break;
case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
break;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT:
connectionStatus.setText("NVT");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
break;
case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound");
connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
connectionStatus.setForeground(ThemeManager.getOiaAttention());
break;
default:
connectionStatus.setText(state.name());
@@ -144,11 +144,11 @@ public class StatusBar extends JPanel {
String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) {
tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else {
tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
tlsStatus.setForeground(ThemeManager.getOiaAttention());
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
}
} else {
@@ -164,38 +164,46 @@ public class StatusBar extends JPanel {
lu = "LU:" + client.getConfig().getLuName();
}
luName.setText(lu);
luName.setForeground(ThemeManager.getOiaFgNormal(theme));
luName.setForeground(ThemeManager.getOiaStatusSysAvail());
// Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited();
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
Color lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
switch (inhibit) {
case ECLConstants.INHIBIT_SYSTEM_LOCK:
lockStatus.setText("X SYSTEM");
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
break;
case ECLConstants.INHIBIT_COMM_CHECK:
lockStatus.setText("X COMM");
lockFg = ThemeManager.getOiaCommCheck(); // Red (oEI)
break;
case ECLConstants.INHIBIT_NUMERIC_ONLY:
lockStatus.setText("X NUM");
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
break;
case ECLConstants.INHIBIT_PROTECTED_FIELD:
lockStatus.setText("X PROT");
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
break;
case ECLConstants.INHIBIT_OVERFLOW:
lockStatus.setText("X >");
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
break;
case ECLConstants.INHIBIT_OPERATOR_DUE:
lockStatus.setText("X OP");
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
break;
default:
lockStatus.setText("X LOCKED");
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
break;
}
lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
lockStatus.setForeground(lockFg);
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
lockStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
} else {
lockStatus.setText("");
}
@@ -223,7 +231,13 @@ public class StatusBar extends JPanel {
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
String modelName;
if (client.getConfig().isDynamicModel() || (client.getConfig().getModel() != null && client.getConfig().getModel().isDynamic())) {
modelName = "IBM-DYNAMIC [" + rows + "x" + cols + "]";
} else {
modelName = client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]";
}
modelInfo.setText(modelName);
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
// Cursor position and buffer address
@@ -57,11 +57,22 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
CHAR_STRINGS[i] = String.valueOf((char) i);
}
}
public enum CursorStyle {
BLOCK,
UNDERLINE
}
private CursorStyle cursorStyle = CursorStyle.BLOCK;
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
private boolean crosshairRulerEnabled = false;
private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
private boolean textBlinkVisible = true;
private Image wallpaperImage = null;
private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
private int selectionStartRow = -1, selectionStartCol = -1;
private int selectionEndRow = -1, selectionEndCol = -1;
private boolean isDragging = false;
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
// ========== Search Highlight state ==========
private int searchHighlightAddr = -1;
@@ -96,31 +107,31 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
return Math.max(padding, extra / 2);
}
// Default Host color mapping
// Default Host color mapping aligned 1:1 with IBM Host On-Demand ColorRemapModel3270
public static final Color[] DEFAULT_HOST_COLORS = {
new Color(0, 0, 0), // 0: Neutral Black
new Color(80, 120, 255), // 1: Blue
new Color(255, 50, 50), // 2: Red
new Color(255, 130, 180), // 3: Pink
new Color(50, 205, 50), // 4: Green
new Color(64, 224, 208), // 5: Turquoise
new Color(255, 255, 80), // 6: Yellow
new Color(120, 144, 240), // 1: Blue (0x7890F0 CUSTOMBLUE)
new Color(255, 0, 0), // 2: Red
new Color(255, 0, 255), // 3: Pink
new Color(0, 255, 0), // 4: Green
new Color(0, 255, 255), // 5: Turquoise / Cyan
new Color(255, 255, 0), // 6: Yellow
new Color(255, 255, 255), // 7: Neutral White
new Color(0, 0, 0), // 8: Black
new Color(30, 60, 180), // 9: Deep Blue
new Color(255, 165, 0), // 10: Orange
new Color(180, 130, 255), // 11: Purple
new Color(144, 238, 144), // 12: Pale Green
new Color(175, 238, 238), // 13: Pale Turquoise
new Color(170, 170, 170), // 14: Grey
new Color(255, 255, 255), // 15: White
new Color(0, 0, 128), // 9: Deep Blue
new Color(255, 162, 0), // 10: Orange (0xFFFFA200)
new Color(128, 0, 128), // 11: Purple
new Color(0, 128, 0), // 12: Pale Green
new Color(0, 128, 128), // 13: Pale Turquoise
new Color(160, 160, 0), // 14: Mustard (0xFFA0A000)
new Color(192, 192, 192), // 15: Grey (0xFFC0C0C0)
};
// Default 3278 monochrome colors
public static final Color DEFAULT_MONO_NORMAL = new Color(50, 205, 50);
public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 255, 255);
public static final Color DEFAULT_MONO_PROTECTED = new Color(80, 120, 255);
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
// Default 3278 / base monochrome colors aligned with HoD
public static final Color DEFAULT_MONO_NORMAL = new Color(0, 255, 0); // Green
public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 0, 0); // Red
public static final Color DEFAULT_MONO_PROTECTED = new Color(0, 255, 255); // Cyan / Turquoise
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255); // White
// Default Background
public static final Color DEFAULT_BG_COLOR = Color.BLACK;
@@ -138,6 +149,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
setupCursorBlink();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
// Handle mouse clicks to position cursor, selection, and grab focus
MouseAdapter mouseHandler = new MouseAdapter() {
@@ -1075,6 +1089,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
setupKeyBindings();
setupFont();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
revalidate();
repaint();
}
@@ -1103,13 +1120,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
private void setupCursorBlink() {
blinkTimer = new Timer(530, e -> {
blinkTimer = new Timer(500, e -> {
cursorVisible = !cursorVisible;
textBlinkVisible = !textBlinkVisible;
repaint();
});
blinkTimer.start();
}
public Telnet3270Client getClient() {
return client;
}
public void setClient(Telnet3270Client client) {
this.client = client;
if (client != null) {
@@ -1247,7 +1269,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int cols = sb.getDisplayCols();
boolean isColorModel = client.getConfig().getModel().isColor();
// Draw Vector Graphics Plane under text if present
// Layer 0: Optional Wallpaper / Background Image
if (hodWallpaper != null) {
int gridW = cols * cellWidth;
int gridH = rows * cellHeight;
hodWallpaper.paint(this, g2, ox, oy, gridW, gridH);
} else if (wallpaperImage != null) {
int gridW = cols * cellWidth;
int gridH = rows * cellHeight;
g2.drawImage(wallpaperImage, ox, oy, gridW, gridH, null);
}
// Layer 1: Draw Vector Graphics Plane under text if present
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) {
int gridW = cols * cellWidth;
int gridH = rows * cellHeight;
@@ -1269,6 +1302,22 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
// Layer 2: Optional Crosshair Ruler under character glyphs
if (crosshairRulerEnabled && client.getConnectionState().isFullSession()) {
int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int cx = ox + curCol * cellWidth;
int cy = oy + curRow * cellHeight;
int gridW = cols * cellWidth;
int gridH = rows * cellHeight;
g2.setColor(CROSSHAIR_RULER_COLOR);
g2.fillRect(ox, cy, gridW, cellHeight);
g2.fillRect(cx, oy, cellWidth, gridH);
}
// Layer 3: Character / Text Plane
byte currentFA = 0;
ExtendedAttribute currentFieldEa = null;
@@ -1296,7 +1345,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
continue;
}
// Determine foreground color
// Determine foreground and background colors
if (isColorModel) {
fgColor = getColorForAttribute(ea, currentFieldEa, currentFA);
bgColor = getBackgroundForAttribute(ea, currentFieldEa);
@@ -1305,6 +1354,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
bgColor = this.bgColor;
}
int rawBg = ea.bg != 0 ? (ea.bg & 0xFF) : (currentFieldEa != null ? (currentFieldEa.bg & 0xFF) : 0);
boolean bgIsExplicit = (rawBg >= 0xF0 && rawBg <= 0xFF && rawBg != HOST_COLOR_NEUTRAL_BLACK && rawBg != HOST_COLOR_BLACK);
// Graphics rendition
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
if (gr != 0) {
@@ -1334,22 +1386,26 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (reverse) {
Color tmp = fgColor;
fgColor = bgColor;
fgColor = (rawBg == 0) ? this.bgColor : bgColor;
bgColor = tmp;
}
if (!bgColor.equals(this.bgColor) || reverse) {
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
} else if (bgIsExplicit || !bgColor.equals(this.bgColor)) {
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
}
// Check text blink (500ms cycle)
boolean blink = (gr & GR_BLINK) != 0;
boolean suppressGlyph = blink && !textBlinkVisible;
// Draw character or Programmed Symbol
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
boolean drawnAsPs = false;
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
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 = (!bgColor.equals(this.bgColor) || reverse) ? bgColor.getRGB() : 0;
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg);
if (img != null) {
g2.drawImage(img, x, y, null);
@@ -1358,7 +1414,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
if (!drawnAsPs) {
if (!drawnAsPs && !suppressGlyph) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? boldTerminalFont : terminalFont;
@@ -1385,7 +1441,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
g2.fillRect(x, y, cellWidth, cellHeight);
}
// Draw selection highlight
// Draw selection highlight (40% alpha blend over cell)
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
@@ -1394,9 +1450,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
// Draw Graphic Cursor if active
if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
int gocaX = client.getGocaDecoder().getGraphicCursorX();
int gocaY = client.getGocaDecoder().getGraphicCursorY();
boolean isGraphicCursor = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
|| (client.getGraphicsPlane() != null && client.getGraphicsPlane().isGraphicCursorAttached());
if (isGraphicCursor && client.getGraphicsPlane() != null) {
int gocaX = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
? client.getGocaDecoder().getGraphicCursorX()
: client.getGraphicsPlane().getGraphicCursorX();
int gocaY = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
? client.getGocaDecoder().getGraphicCursorY()
: client.getGraphicsPlane().getGraphicCursorY();
int canvasPx = client.getGraphicsPlane().mapX(gocaX);
int canvasPy = client.getGraphicsPlane().mapY(gocaY);
int gridW = cols * cellWidth;
@@ -1408,12 +1470,19 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
g2.setColor(Color.WHITE);
g2.setXORMode(Color.BLACK);
g2.drawLine(px - 6, py, px + 6, py);
g2.drawLine(px, py - 6, px, py + 6);
int shape = client.getGraphicsPlane().getHodCursorShape();
if (shape == 2) {
// Shape 2: Box cursor per HoD
g2.drawRect(px - 3, py - 3, 6, 6);
} else {
// Shape 1 (default): Crosshair per HoD
g2.drawLine(px - 8, py, px + 8, py);
g2.drawLine(px, py - 8, px, py + 8);
}
g2.setPaintMode();
}
// Draw 3270 text cursor
// Draw 3270 text cursor with alpha blending (Block / Underline)
if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols;
@@ -1421,13 +1490,79 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int cx = ox + curCol * cellWidth;
int cy = oy + curRow * cellHeight;
boolean isUnderline = (cursorStyle == CursorStyle.UNDERLINE) ||
(client.getInputProcessor() != null && client.getInputProcessor().isInsertMode());
g2.setColor(CURSOR_COLOR);
g2.setXORMode(bgColor);
g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.setPaintMode();
if (isUnderline) {
int ulH = Math.max(2, cellHeight / 6);
g2.fillRect(cx, cy + cellHeight - ulH, cellWidth, ulH);
} else {
g2.fillRect(cx, cy, cellWidth, cellHeight);
}
}
}
public boolean isCrosshairRulerEnabled() {
return crosshairRulerEnabled;
}
public void setCrosshairRulerEnabled(boolean enabled) {
this.crosshairRulerEnabled = enabled;
repaint();
}
public void toggleCrosshairRuler() {
setCrosshairRulerEnabled(!crosshairRulerEnabled);
}
public CursorStyle getCursorStyle() {
return cursorStyle;
}
public void setCursorStyle(CursorStyle style) {
this.cursorStyle = (style != null) ? style : CursorStyle.BLOCK;
repaint();
}
public Image getWallpaperImage() {
return wallpaperImage;
}
public void setWallpaperImage(Image wallpaperImage) {
this.wallpaperImage = wallpaperImage;
if (wallpaperImage != null) {
if (this.hodWallpaper == null) {
this.hodWallpaper = new haus.nightmare.lib3270j.graphics.HODWallpaper(wallpaperImage, haus.nightmare.lib3270j.graphics.HODWallpaper.HOD_STRETCH);
} else {
this.hodWallpaper.setImage(wallpaperImage);
}
} else {
this.hodWallpaper = null;
}
repaint();
}
public haus.nightmare.lib3270j.graphics.HODWallpaper getHodWallpaper() {
return hodWallpaper;
}
public void setHodWallpaper(haus.nightmare.lib3270j.graphics.HODWallpaper wallpaper) {
this.hodWallpaper = wallpaper;
if (wallpaper != null) {
this.wallpaperImage = wallpaper.getHODImage();
}
repaint();
}
public void setWallpaperMode(int displayMode) {
if (this.hodWallpaper == null && this.wallpaperImage != null) {
this.hodWallpaper = new haus.nightmare.lib3270j.graphics.HODWallpaper(this.wallpaperImage, displayMode);
} else if (this.hodWallpaper != null) {
this.hodWallpaper.setDisplay(displayMode);
}
repaint();
}
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
int fg = ea.fg != 0 ? (ea.fg & 0xFF)
: (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
@@ -1435,7 +1570,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
return hostColors[fg - 0xf0];
}
if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_BLUE];
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_TURQUOISE];
}
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_RED] : hostColors[HOST_COLOR_GREEN];
}
@@ -244,6 +244,19 @@ public final class ThemeManager {
return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
}
// Host On-Demand OIA Category Colors (oSI, oII, oAI, oEI, oOB)
public static final Color HOD_OIA_STATUS_SYS_AVAIL = new Color(120, 144, 240); // oSI: CUSTOMBLUE
public static final Color HOD_OIA_INPUT_INHIBITED = new Color(255, 255, 255); // oII: White
public static final Color HOD_OIA_ATTENTION_WARN = new Color(255, 255, 0); // oAI: Yellow
public static final Color HOD_OIA_COMM_CHECK_ERROR = new Color(255, 0, 0); // oEI: Red
public static final Color HOD_OIA_BG_BLACK = new Color(0, 0, 0); // oOB: Black
public static Color getOiaStatusSysAvail() { return HOD_OIA_STATUS_SYS_AVAIL; }
public static Color getOiaInputInhibited() { return HOD_OIA_INPUT_INHIBITED; }
public static Color getOiaAttention() { return HOD_OIA_ATTENTION_WARN; }
public static Color getOiaCommCheck() { return HOD_OIA_COMM_CHECK_ERROR; }
public static Color getOiaBackground() { return HOD_OIA_BG_BLACK; }
// =========================================================================
// Button Variants & Color helpers
// =========================================================================
@@ -58,12 +58,26 @@ public class MenuBarShortcutsTest {
assertNotNull(fileMenu, "File menu should exist");
assertNotNull(editMenu, "Edit menu should exist");
// Verify View Menu items use ALT_DOWN_MASK
// Verify View Menu items
Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu);
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
// Crosshair Ruler must use Alt+Shift+R
JMenuItem rulerItem = viewItems.get("Crosshair Ruler");
assertNotNull(rulerItem, "Crosshair Ruler menu item must exist");
KeyStroke rulerKs = rulerItem.getAccelerator();
assertNotNull(rulerKs, "Crosshair Ruler should have an accelerator");
assertEquals(KeyEvent.VK_R, rulerKs.getKeyCode(), "Key code mismatch for Crosshair Ruler");
int rulerMods = rulerKs.getModifiers();
assertTrue((rulerMods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Crosshair Ruler accelerator must have ALT modifier");
assertTrue((rulerMods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK)) != 0,
"Crosshair Ruler accelerator must have SHIFT modifier");
assertEquals(0, rulerMods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
"Crosshair Ruler accelerator must NOT have CTRL or META modifiers");
// Verify Actions Menu items use ALT_DOWN_MASK
Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu);
assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER);
@@ -75,6 +89,10 @@ public class MenuBarShortcutsTest {
assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
// Ensure Crosshair Ruler and Reset do not collide
assertNotEquals(rulerKs, actionItems.get("Reset").getAccelerator(),
"Crosshair Ruler and Reset accelerators must not collide");
} finally {
// Dispose frame
app.dispose();
@@ -89,6 +107,8 @@ public class MenuBarShortcutsTest {
int mods = ks.getModifiers();
assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Menu item " + item.getText() + " accelerator must have ALT modifier");
assertEquals(0, mods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK),
"Menu item " + item.getText() + " accelerator must NOT have SHIFT modifier");
assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
"Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
}
@@ -0,0 +1,104 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Color;
import java.awt.HeadlessException;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 1 UI Overlays and Color Alignment.
* Covers items 1.1, 1.2, and 1.5 of the PhasedUpdates specification.
*/
public class Phase1UiOverlayTest {
@Test
@DisplayName("Item 1.1 & 1.2: TerminalPanel host and mono colors match IBM Host On-Demand specs")
public void testTerminalPanelColorAlignment() {
// Index 10: Orange (255, 162, 0)
assertEquals(new Color(255, 162, 0), TerminalPanel.DEFAULT_HOST_COLORS[10]);
// Index 14: Mustard (160, 160, 0)
assertEquals(new Color(160, 160, 0), TerminalPanel.DEFAULT_HOST_COLORS[14]);
// Index 1: Blue (120, 144, 240)
assertEquals(new Color(120, 144, 240), TerminalPanel.DEFAULT_HOST_COLORS[1]);
// Base 4-Color mono defaults
assertEquals(new Color(0, 255, 0), TerminalPanel.DEFAULT_MONO_NORMAL);
assertEquals(new Color(255, 0, 0), TerminalPanel.DEFAULT_MONO_INTENSIFY);
assertEquals(new Color(0, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED);
assertEquals(new Color(255, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH);
}
@Test
@DisplayName("Item 1.5: ThemeManager HoD OIA category colors match specification")
public void testThemeManagerOiaColors() {
// oSI: Status / System Available -> CUSTOMBLUE (120, 144, 240)
assertEquals(new Color(120, 144, 240), ThemeManager.getOiaStatusSysAvail());
// oII: Input Inhibited / X SYSTEM -> White (255, 255, 255)
assertEquals(new Color(255, 255, 255), ThemeManager.getOiaInputInhibited());
// oAI: Attention / Reminders / Message Waiting -> Yellow (255, 255, 0)
assertEquals(new Color(255, 255, 0), ThemeManager.getOiaAttention());
// oEI: Error Checks / Comm Check -> Red (255, 0, 0)
assertEquals(new Color(255, 0, 0), ThemeManager.getOiaCommCheck());
// oOB: OIA Separator / Background -> Black (0, 0, 0)
assertEquals(new Color(0, 0, 0), ThemeManager.getOiaBackground());
}
@Test
@DisplayName("Item 1.5: TerminalPanel Crosshair Ruler and Cursor Style controls")
public void testTerminalPanelRulerAndCursorStyle() {
try {
TerminalPanel panel = new TerminalPanel();
// Default cursor style is BLOCK
assertEquals(TerminalPanel.CursorStyle.BLOCK, panel.getCursorStyle());
panel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE);
assertEquals(TerminalPanel.CursorStyle.UNDERLINE, panel.getCursorStyle());
// Crosshair ruler toggling
assertFalse(panel.isCrosshairRulerEnabled());
panel.setCrosshairRulerEnabled(true);
assertTrue(panel.isCrosshairRulerEnabled());
panel.toggleCrosshairRuler();
assertFalse(panel.isCrosshairRulerEnabled());
// Wallpaper image setter
assertNull(panel.getWallpaperImage());
} catch (HeadlessException e) {
// Handled gracefully in headless CI environments
}
}
@Test
@DisplayName("Settings persistence for Crosshair Ruler and Cursor Style")
public void testSettingsPersistence() {
boolean originalRuler = Settings.getCrosshairRuler();
String originalCursor = Settings.getCursorStyle();
try {
Settings.setCrosshairRuler(true);
assertTrue(Settings.getCrosshairRuler());
Settings.setCrosshairRuler(false);
assertFalse(Settings.getCrosshairRuler());
Settings.setCursorStyle("UNDERLINE");
assertEquals("UNDERLINE", Settings.getCursorStyle());
Settings.setCursorStyle("BLOCK");
assertEquals("BLOCK", Settings.getCursorStyle());
} finally {
Settings.setCrosshairRuler(originalRuler);
Settings.setCursorStyle(originalCursor);
}
}
}
@@ -0,0 +1,95 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.graphics.GocaConstants;
import haus.nightmare.lib3270j.graphics.GraphicsPlane;
import haus.nightmare.lib3270j.graphics.HODWallpaper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.image.BufferedImage;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 2 UI Vector Graphics, Wallpaper, and Cursor Overlays.
*/
public class Phase2UiGraphicsTest {
@Test
@DisplayName("Test TerminalPanel Wallpaper Integration (Tile, Center, Stretch)")
public void testTerminalPanelWallpaperModes() {
try {
TerminalPanel panel = new TerminalPanel();
assertNull(panel.getHodWallpaper());
assertNull(panel.getWallpaperImage());
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
panel.setWallpaperImage(img);
assertNotNull(panel.getWallpaperImage());
assertNotNull(panel.getHodWallpaper());
assertEquals(HODWallpaper.HOD_STRETCH, panel.getHodWallpaper().getDisplay());
panel.setWallpaperMode(HODWallpaper.HOD_TILE);
assertEquals(HODWallpaper.HOD_TILE, panel.getHodWallpaper().getDisplay());
panel.setWallpaperMode(HODWallpaper.HOD_CENTER);
assertEquals(HODWallpaper.HOD_CENTER, panel.getHodWallpaper().getDisplay());
HODWallpaper customWp = new HODWallpaper(img, HODWallpaper.HOD_TILE);
panel.setHodWallpaper(customWp);
assertSame(customWp, panel.getHodWallpaper());
// Clear wallpaper
panel.setWallpaperImage(null);
assertNull(panel.getHodWallpaper());
assertNull(panel.getWallpaperImage());
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
@Test
@DisplayName("Test Graphics Plane Cursor Synchronization and Rendering")
public void testGraphicsCursorOverlay() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(new Dimension(800, 600));
panel.setClient(new haus.nightmare.lib3270j.Telnet3270Client(new haus.nightmare.lib3270j.ConnectionConfig("localhost", 23)));
GraphicsPlane plane = panel.getClient() != null ? panel.getClient().getGraphicsPlane() : null;
if (plane != null) {
// Attach graphics cursor
plane.attachGraphicCursor(100, 100);
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_CROSSHAIR);
assertTrue(plane.isGraphicCursorAttached());
assertEquals(100, plane.getGraphicCursorX());
assertEquals(100, plane.getGraphicCursorY());
assertEquals(1, plane.getHodCursorShape());
// Paint component to verify no exceptions during rendering
BufferedImage offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = offscreen.createGraphics();
panel.paint(g2);
g2.dispose();
// Change cursor shape to box
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_BOX);
assertEquals(2, plane.getHodCursorShape());
offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
g2 = offscreen.createGraphics();
panel.paint(g2);
g2.dispose();
// Detach graphics cursor
plane.detachGraphicCursor();
assertFalse(plane.isGraphicCursorAttached());
}
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
}