2 Commits

Author SHA1 Message Date
rudi 7b5bc7ec11 Add consts and update tests
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m20s
2026-08-31 10:22:12 -04:00
rudi a9956298e2 More functional changes and UI adaptations from overnight 2026-08-31 09:16:32 -04:00
54 changed files with 8015 additions and 1209 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ public class TestRunner {
public static void main(String[] args) { public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors( .selectors(
selectPackage("haus.nightmare.lib3270j") selectPackage("haus.nightmare")
) )
.build(); .build();
@@ -53,6 +53,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
buildUI(); buildUI();
buildMenuBar(); buildMenuBar();
ThemeManager.addThemeChangeListener(this::onThemeChanged);
pack(); pack();
setLocationRelativeTo(null); setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400)); setMinimumSize(new Dimension(640, 400));
@@ -97,10 +99,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
getContentPane().add(statusBar, BorderLayout.SOUTH); getContentPane().add(statusBar, BorderLayout.SOUTH);
} }
private void onThemeChanged(UITheme theme) {
buildMenuBar();
statusBar.applyTheme(theme);
ThemeManager.applyThemeToWindow(this);
terminalPanel.repaint();
}
private void buildMenuBar() { private void buildMenuBar() {
JMenuBar menuBar = new JMenuBar(); JMenuBar menuBar = new JMenuBar();
menuBar.setBackground(new Color(30, 30, 30)); ThemeManager.styleMenuBar(menuBar);
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
@@ -145,6 +153,31 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
})); }));
viewMenu.addSeparator(); viewMenu.addSeparator();
// UI Theme Submenu
JMenu themeMenu = createMenu("UI Theme");
ButtonGroup themeGroup = new ButtonGroup();
JRadioButtonMenuItem darkThemeItem = new JRadioButtonMenuItem("Dark Mode", ThemeManager.isDark());
ThemeManager.styleMenuItem(darkThemeItem);
darkThemeItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.DARK);
ThemeManager.setTheme(UITheme.DARK);
});
themeGroup.add(darkThemeItem);
themeMenu.add(darkThemeItem);
JRadioButtonMenuItem lightThemeItem = new JRadioButtonMenuItem("Light Mode", ThemeManager.isLight());
ThemeManager.styleMenuItem(lightThemeItem);
lightThemeItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.LIGHT);
ThemeManager.setTheme(UITheme.LIGHT);
});
themeGroup.add(lightThemeItem);
themeMenu.add(lightThemeItem);
viewMenu.add(themeMenu);
viewMenu.addSeparator();
// CodePage Submenu // CodePage Submenu
JMenu cpMenu = createMenu("Code Page"); JMenu cpMenu = createMenu("Code Page");
String[] codePages = { String[] codePages = {
@@ -174,8 +207,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
for (String cpItem : codePages) { for (String cpItem : codePages) {
String cpId = cpItem.split(" -")[0].trim(); String cpId = cpItem.split(" -")[0].trim();
JMenuItem cpMi = new JMenuItem(cpItem); JMenuItem cpMi = new JMenuItem(cpItem);
cpMi.setBackground(new Color(40, 40, 40)); ThemeManager.styleMenuItem(cpMi);
cpMi.setForeground(new Color(200, 200, 200));
cpMi.addActionListener(e -> changeCodePage(cpId)); cpMi.addActionListener(e -> changeCodePage(cpId));
cpMenu.add(cpMi); cpMenu.add(cpMi);
} }
@@ -185,8 +217,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JMenu gfxMenu = createMenu("Graphics Mode"); JMenu gfxMenu = createMenu("Graphics Mode");
for (GraphicsMode gm : GraphicsMode.values()) { for (GraphicsMode gm : GraphicsMode.values()) {
JMenuItem gmMi = new JMenuItem(gm.name()); JMenuItem gmMi = new JMenuItem(gm.name());
gmMi.setBackground(new Color(40, 40, 40)); ThemeManager.styleMenuItem(gmMi);
gmMi.setForeground(new Color(200, 200, 200));
gmMi.addActionListener(e -> changeGraphicsMode(gm)); gmMi.addActionListener(e -> changeGraphicsMode(gm));
gfxMenu.add(gmMi); gfxMenu.add(gmMi);
} }
@@ -257,14 +288,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private JMenu createMenu(String name) { private JMenu createMenu(String name) {
JMenu menu = new JMenu(name); JMenu menu = new JMenu(name);
menu.setForeground(new Color(200, 200, 200)); return ThemeManager.styleMenu(menu);
return menu;
} }
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) { private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
JMenuItem item = new JMenuItem(name); JMenuItem item = new JMenuItem(name);
item.setBackground(new Color(40, 40, 40)); ThemeManager.styleMenuItem(item);
item.setForeground(new Color(200, 200, 200));
if (mnemonic > 0) { if (mnemonic > 0) {
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier)); item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
@@ -693,9 +722,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30)); ThemeManager.styleTextArea(area);
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp);
sp.setPreferredSize(new Dimension(380, 430)); sp.setPreferredSize(new Dimension(380, 430));
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
} }
@@ -726,9 +755,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30)); ThemeManager.styleTextArea(area);
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp);
sp.setPreferredSize(new Dimension(440, 430)); sp.setPreferredSize(new Dimension(440, 430));
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
} }
@@ -758,45 +787,47 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
Boolean cliTn3270e = null; Boolean cliTn3270e = null;
GraphicsMode cliGraphicsMode = null; GraphicsMode cliGraphicsMode = null;
String configFile = null; String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>(); java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
if ("--debug".equals(args[i]) || "-d".equals(args[i])) { String arg = args[i];
if ("-d".equals(arg) || "--debug".equals(arg)) {
debug = true; debug = true;
} else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) { } else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
cliTls = true; cliTls = true;
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) { } else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
cliNoVerifyCert = true; cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) { } else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
cliTn3270e = false; cliTn3270e = false;
} else if ("--tn3270e".equals(args[i])) { } else if ("--tn3270e".equals(arg)) {
cliTn3270e = true; cliTn3270e = true;
} else if (args[i].startsWith("--graphics=")) { } else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(args[i].substring(11)); cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) { } else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]); cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(args[i])) { } else if ("--no-graphics".equals(arg)) {
cliGraphicsMode = GraphicsMode.NONE; cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) { } else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i]; configFile = args[++i];
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} else { } else {
remainingArgs.add(args[i]); remainingArgs.add(arg);
} }
} }
Level logLevel = debug ? Level.ALL : Level.INFO; Level logLevel = debug ? Level.FINE : Level.INFO;
Logger globalRoot = Logger.getLogger(""); Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) { for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h); globalRoot.removeHandler(h);
} }
globalRoot.setLevel(Level.ALL);
java.util.logging.Filter appFilter = record -> { java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
String name = record.getLoggerName(); (record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
return name != null && (name.startsWith("haus.nightmare.lib3270j") || name.startsWith("haus.nightmare.j3270"));
};
ConsoleHandler consoleHandler = new ConsoleHandler(); ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(logLevel); consoleHandler.setLevel(Level.ALL);
consoleHandler.setFormatter(new SimpleFormatter()); consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter); consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler); globalRoot.addHandler(consoleHandler);
@@ -837,6 +868,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} catch (Exception e) { } catch (Exception e) {
log.fine("Could not set system look and feel"); log.fine("Could not set system look and feel");
} }
ThemeManager.applyUIManagerDefaults(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
ThemeManager.setTheme(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("apple.awt.application.name", "j3270"); System.setProperty("apple.awt.application.name", "j3270");
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.config; package haus.nightmare.j3270.config;
import haus.nightmare.j3270.ui.UITheme;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import java.awt.Color; import java.awt.Color;
import java.io.*; import java.io.*;
@@ -17,6 +18,15 @@ public class Settings {
AUTO_CONNECT AUTO_CONNECT
} }
public static UITheme getJavaUiTheme() {
String themeStr = prefs.get("javaUiTheme", UITheme.DARK.name());
return UITheme.fromString(themeStr);
}
public static void setJavaUiTheme(UITheme theme) {
prefs.put("javaUiTheme", (theme != null ? theme : UITheme.DARK).name());
}
public static String getFontFamily() { public static String getFontFamily() {
return prefs.get("fontFamily", "Monospaced"); return prefs.get("fontFamily", "Monospaced");
} }
@@ -229,6 +239,11 @@ public class Settings {
switch (key) { switch (key) {
case "fontFamily": setFontFamily(value); break; case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break; case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "javaUiTheme":
case "theme":
case "uiTheme":
setJavaUiTheme(UITheme.fromString(value));
break;
default: default:
log.warning("Unknown appearance key: " + key); log.warning("Unknown appearance key: " + key);
} }
@@ -322,6 +337,7 @@ public class Settings {
// [appearance] // [appearance]
w.println("[appearance]"); w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("fontFamily = " + getFontFamily()); w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize()); w.println("fontSize = " + getFontSize());
w.println(); w.println();
@@ -1,6 +1,7 @@
package haus.nightmare.j3270.ft; package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.HostDirectoryDialog; import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants; import haus.nightmare.lib3270j.ft.FTConstants;
@@ -88,6 +89,7 @@ public class FileTransferDialog extends JDialog {
// Local File // Local File
localFileField = new JTextField(20); localFileField = new JTextField(20);
browseLocalButton = new JButton("Browse..."); browseLocalButton = new JButton("Browse...");
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
browseLocalButton.addActionListener(e -> browseLocalFile()); browseLocalButton.addActionListener(e -> browseLocalFile());
JPanel localPanel = new JPanel(new BorderLayout(5, 0)); JPanel localPanel = new JPanel(new BorderLayout(5, 0));
localPanel.setOpaque(false); localPanel.setOpaque(false);
@@ -98,6 +100,7 @@ public class FileTransferDialog extends JDialog {
// Host File // Host File
hostFileField = new JTextField(20); hostFileField = new JTextField(20);
browseHostButton = new JButton("Browse Host..."); browseHostButton = new JButton("Browse Host...");
ThemeManager.styleButton(browseHostButton, ThemeManager.ButtonVariant.DEFAULT);
browseHostButton.addActionListener(e -> browseHostDirectory()); browseHostButton.addActionListener(e -> browseHostDirectory());
JPanel hostPanel = new JPanel(new BorderLayout(5, 0)); JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
hostPanel.setOpaque(false); hostPanel.setOpaque(false);
@@ -154,10 +157,7 @@ public class FileTransferDialog extends JDialog {
// Host-specific options panel (TSO dataset allocation) // Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5)); JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
hostOptsPanel.setOpaque(false); hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(BorderFactory.createTitledBorder( hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"TSO Allocation Options (Send Only)"));
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
hostOptsPanel.add(new JLabel("RECFM:")); hostOptsPanel.add(new JLabel("RECFM:"));
recfmField = new JTextField(5); recfmField = new JTextField(5);
@@ -181,10 +181,7 @@ public class FileTransferDialog extends JDialog {
// CMS options // CMS options
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0)); JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
vmOptsPanel.setOpaque(false); vmOptsPanel.setOpaque(false);
vmOptsPanel.setBorder(BorderFactory.createTitledBorder( vmOptsPanel.setBorder(ThemeManager.createTitledBorder("CMS Options"));
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"CMS Options"));
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
optionsField = new JTextField(20); optionsField = new JTextField(20);
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST); vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
@@ -200,9 +197,11 @@ public class FileTransferDialog extends JDialog {
buttonPanel.setOpaque(false); buttonPanel.setOpaque(false);
transferButton = new JButton("Start Transfer"); transferButton = new JButton("Start Transfer");
ThemeManager.styleButton(transferButton, ThemeManager.ButtonVariant.PRIMARY);
transferButton.addActionListener(e -> startTransfer()); transferButton.addActionListener(e -> startTransfer());
cancelButton = new JButton("Close"); cancelButton = new JButton("Close");
ThemeManager.styleButton(cancelButton, ThemeManager.ButtonVariant.CANCEL);
cancelButton.addActionListener(e -> dispose()); cancelButton.addActionListener(e -> dispose());
buttonPanel.add(cancelButton); buttonPanel.add(cancelButton);
@@ -212,7 +211,7 @@ public class FileTransferDialog extends JDialog {
setContentPane(mainPanel); setContentPane(mainPanel);
applyTheme(mainPanel); ThemeManager.applyThemeToWindow(this);
updateOptionStates(); updateOptionStates();
} }
@@ -34,24 +34,21 @@ public class ConnectDialog extends JDialog {
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout()); JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(5, 6, 5, 6);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14); Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
Color fg = new Color(200, 200, 200);
// Host // Host
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = 0;
JLabel hostLabel = new JLabel("Host:"); JLabel hostLabel = new JLabel("Host:");
hostLabel.setForeground(fg);
hostLabel.setFont(labelFont); hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc); mainPanel.add(hostLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
hostField = createDarkField(20); hostField = createField(20);
mainPanel.add(hostField, gbc); mainPanel.add(hostField, gbc);
// Port // Port
@@ -59,12 +56,11 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 1; gbc.gridy = 1;
gbc.weightx = 0; gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:"); JLabel portLabel = new JLabel("Port:");
portLabel.setForeground(fg);
portLabel.setFont(labelFont); portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc); mainPanel.add(portLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
portField = createDarkField(6); portField = createField(6);
portField.setText("23"); portField.setText("23");
mainPanel.add(portField, gbc); mainPanel.add(portField, gbc);
@@ -73,16 +69,14 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 2; gbc.gridy = 2;
gbc.weightx = 0; gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:"); JLabel modelLabel = new JLabel("Model:");
modelLabel.setForeground(fg);
modelLabel.setFont(labelFont); modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc); mainPanel.add(modelLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values()); modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setBackground(new Color(45, 45, 45));
modelCombo.setForeground(fg);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc); mainPanel.add(modelCombo, gbc);
// LU Name // LU Name
@@ -90,12 +84,11 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 3; gbc.gridy = 3;
gbc.weightx = 0; gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:"); JLabel luLabel = new JLabel("LU Name:");
luLabel.setForeground(fg);
luLabel.setFont(labelFont); luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc); mainPanel.add(luLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
luField = createDarkField(12); luField = createField(12);
mainPanel.add(luField, gbc); mainPanel.add(luField, gbc);
// Graphics Mode // Graphics Mode
@@ -103,16 +96,14 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 4; gbc.gridy = 4;
gbc.weightx = 0; gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:"); JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setForeground(fg);
graphicsLabel.setFont(labelFont); graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc); mainPanel.add(graphicsLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values()); graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setBackground(new Color(45, 45, 45));
graphicsCombo.setForeground(fg);
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc); mainPanel.add(graphicsCombo, gbc);
// Code Page // Code Page
@@ -120,7 +111,6 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 5; gbc.gridy = 5;
gbc.weightx = 0; gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:"); JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setForeground(fg);
cpLabel.setFont(labelFont); cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc); mainPanel.add(cpLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
@@ -158,9 +148,8 @@ public class ConnectDialog extends JDialog {
break; break;
} }
} }
codePageCombo.setBackground(new Color(45, 45, 45));
codePageCombo.setForeground(fg);
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc); mainPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox // TLS / SSL Checkbox
@@ -168,10 +157,8 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 6; gbc.gridy = 6;
gbc.weightx = 1.0; gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL"); tlsCheckBox = new JCheckBox("Enable TLS/SSL");
tlsCheckBox.setBackground(new Color(30, 30, 30)); ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setForeground(fg);
tlsCheckBox.setFont(labelFont); tlsCheckBox.setFont(labelFont);
tlsCheckBox.setFocusPainted(false);
tlsCheckBox.addActionListener(e -> { tlsCheckBox.addActionListener(e -> {
boolean isTls = tlsCheckBox.isSelected(); boolean isTls = tlsCheckBox.isSelected();
verifyCertCheckBox.setEnabled(isTls); verifyCertCheckBox.setEnabled(isTls);
@@ -188,38 +175,31 @@ public class ConnectDialog extends JDialog {
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 7; gbc.gridy = 7;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
verifyCertCheckBox.setBackground(new Color(30, 30, 30)); ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
verifyCertCheckBox.setSelected(true); verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false); verifyCertCheckBox.setEnabled(false);
verifyCertCheckBox.setFocusPainted(false);
mainPanel.add(verifyCertCheckBox, gbc); mainPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox // TN3270E Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 8; gbc.gridy = 8;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
tn3270eCheckBox.setBackground(new Color(30, 30, 30)); ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setForeground(fg);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e()); tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
tn3270eCheckBox.setFocusPainted(false);
mainPanel.add(tn3270eCheckBox, gbc); mainPanel.add(tn3270eCheckBox, gbc);
// Buttons // Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
buttonPanel.setBackground(new Color(30, 30, 30));
JButton connectBtn = new JButton("Connect"); JButton connectBtn = new JButton("Connect");
connectBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.setForeground(Color.WHITE);
connectBtn.setFont(labelFont); connectBtn.setFont(labelFont);
connectBtn.addActionListener(e -> onConnect()); connectBtn.addActionListener(e -> onConnect());
JButton cancelBtn = new JButton("Cancel"); JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setForeground(fg);
cancelBtn.setFont(labelFont); cancelBtn.setFont(labelFont);
cancelBtn.addActionListener(e -> { cancelBtn.addActionListener(e -> {
confirmed = false; confirmed = false;
@@ -235,20 +215,16 @@ public class ConnectDialog extends JDialog {
mainPanel.add(buttonPanel, gbc); mainPanel.add(buttonPanel, gbc);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect // Enter key triggers connect
getRootPane().setDefaultButton(connectBtn); getRootPane().setDefaultButton(connectBtn);
} }
private JTextField createDarkField(int cols) { private JTextField createField(int cols) {
JTextField field = new JTextField(cols); JTextField field = new JTextField(cols);
field.setBackground(new Color(45, 45, 45));
field.setForeground(new Color(200, 200, 200));
field.setCaretColor(new Color(200, 200, 200));
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
field.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(field);
BorderFactory.createLineBorder(new Color(60, 60, 60)),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
return field; return field;
} }
@@ -28,12 +28,6 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
private JTable table; private JTable table;
private JLabel countLabel; private JLabel countLabel;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_BORDER = new Color(65, 65, 65);
private static final Color DARK_SELECTION = new Color(75, 110, 175);
public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) { public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "3270 Presentation Space Field Inspector", false); super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client; this.client = client;
@@ -61,19 +55,16 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(8, 8)); JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(DARK_BG);
// Top info bar // Top info bar
JPanel topPanel = new JPanel(new BorderLayout()); JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen"); countLabel = new JLabel("0 fields detected on screen");
countLabel.setForeground(DARK_FG);
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
topPanel.add(countLabel, BorderLayout.WEST); topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh"); JButton refreshBtn = new JButton("Refresh");
refreshBtn.setBackground(new Color(55, 55, 55)); ThemeManager.styleButton(refreshBtn, ThemeManager.ButtonVariant.DEFAULT);
refreshBtn.setForeground(DARK_FG);
refreshBtn.addActionListener(e -> refreshFields()); refreshBtn.addActionListener(e -> refreshFields());
topPanel.add(refreshBtn, BorderLayout.EAST); topPanel.add(refreshBtn, BorderLayout.EAST);
mainPanel.add(topPanel, BorderLayout.NORTH); mainPanel.add(topPanel, BorderLayout.NORTH);
@@ -91,18 +82,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
}; };
table = new JTable(tableModel); table = new JTable(tableModel);
table.setBackground(DARK_FIELD_BG); ThemeManager.styleTable(table);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(20);
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(DARK_FG);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
// Column widths // Column widths
@@ -125,8 +110,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
}); });
JScrollPane scrollPane = new JScrollPane(table); JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(DARK_BORDER)); ThemeManager.styleScrollPane(scrollPane);
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom panel // Bottom panel
@@ -134,8 +118,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
JButton jumpBtn = new JButton("Jump to Selected Field"); JButton jumpBtn = new JButton("Jump to Selected Field");
jumpBtn.setBackground(new Color(50, 100, 160)); ThemeManager.styleButton(jumpBtn, ThemeManager.ButtonVariant.PRIMARY);
jumpBtn.setForeground(Color.WHITE);
jumpBtn.addActionListener(e -> { jumpBtn.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row >= 0) { if (row >= 0) {
@@ -144,8 +127,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
}); });
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(55, 55, 55)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
bottomPanel.add(jumpBtn); bottomPanel.add(jumpBtn);
@@ -153,6 +135,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
} }
public synchronized void refreshFields() { public synchronized void refreshFields() {
@@ -42,7 +42,6 @@ public class FindDialog extends JDialog {
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
// Form // Form
JPanel formPanel = new JPanel(new GridBagLayout()); JPanel formPanel = new JPanel(new GridBagLayout());
@@ -51,14 +50,12 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Search text // Search text
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = 0;
JLabel findLabel = new JLabel("Find what:"); JLabel findLabel = new JLabel("Find what:");
findLabel.setForeground(fg);
findLabel.setFont(labelFont); findLabel.setFont(labelFont);
formPanel.add(findLabel, gbc); formPanel.add(findLabel, gbc);
@@ -66,23 +63,16 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
searchField = new JTextField(20); searchField = new JTextField(20);
searchField.setText(lastSearchText); searchField.setText(lastSearchText);
searchField.setBackground(new Color(50, 50, 50));
searchField.setForeground(fg);
searchField.setCaretColor(fg);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
searchField.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(searchField);
BorderFactory.createLineBorder(new Color(70, 70, 70)),
BorderFactory.createEmptyBorder(3, 6, 3, 6)));
formPanel.add(searchField, gbc); formPanel.add(searchField, gbc);
// Options // Options
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 1; gbc.gridy = 1;
matchCaseCheck = new JCheckBox("Match case", lastMatchCase); matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
matchCaseCheck.setForeground(fg); ThemeManager.styleCheckBox(matchCaseCheck);
matchCaseCheck.setOpaque(false);
matchCaseCheck.setFont(labelFont); matchCaseCheck.setFont(labelFont);
matchCaseCheck.setFocusPainted(false);
formPanel.add(matchCaseCheck, gbc); formPanel.add(matchCaseCheck, gbc);
// Direction // Direction
@@ -91,16 +81,11 @@ public class FindDialog extends JDialog {
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false); dirPanel.setOpaque(false);
JLabel dirLabel = new JLabel("Direction: "); JLabel dirLabel = new JLabel("Direction: ");
dirLabel.setForeground(fg);
dirLabel.setFont(labelFont); dirLabel.setFont(labelFont);
forwardRadio = new JRadioButton("Down", lastForward); forwardRadio = new JRadioButton("Down", lastForward);
backwardRadio = new JRadioButton("Up", !lastForward); backwardRadio = new JRadioButton("Up", !lastForward);
forwardRadio.setForeground(fg); ThemeManager.styleRadioButton(forwardRadio);
backwardRadio.setForeground(fg); ThemeManager.styleRadioButton(backwardRadio);
forwardRadio.setOpaque(false);
backwardRadio.setOpaque(false);
forwardRadio.setFocusPainted(false);
backwardRadio.setFocusPainted(false);
ButtonGroup bg = new ButtonGroup(); ButtonGroup bg = new ButtonGroup();
bg.add(forwardRadio); bg.add(forwardRadio);
@@ -116,7 +101,7 @@ public class FindDialog extends JDialog {
gbc.gridy = 3; gbc.gridy = 3;
gbc.gridwidth = 2; gbc.gridwidth = 2;
statusLabel = new JLabel(" "); statusLabel = new JLabel(" ");
statusLabel.setForeground(new Color(255, 120, 120)); statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
formPanel.add(statusLabel, gbc); formPanel.add(statusLabel, gbc);
@@ -127,14 +112,12 @@ public class FindDialog extends JDialog {
buttonPanel.setOpaque(false); buttonPanel.setOpaque(false);
JButton findNextBtn = new JButton("Find Next"); JButton findNextBtn = new JButton("Find Next");
findNextBtn.setBackground(new Color(60, 63, 65)); ThemeManager.styleButton(findNextBtn, ThemeManager.ButtonVariant.PRIMARY);
findNextBtn.setForeground(fg);
findNextBtn.setFont(labelFont); findNextBtn.setFont(labelFont);
findNextBtn.addActionListener(e -> findNext()); findNextBtn.addActionListener(e -> findNext());
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 63, 65)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont); closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
@@ -144,6 +127,7 @@ public class FindDialog extends JDialog {
mainPanel.add(buttonPanel, BorderLayout.SOUTH); mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
getRootPane().setDefaultButton(findNextBtn); getRootPane().setDefaultButton(findNextBtn);
searchField.addKeyListener(new KeyAdapter() { searchField.addKeyListener(new KeyAdapter() {
@@ -49,9 +49,7 @@ public class HostDirectoryDialog extends JDialog {
private void buildUI(String initialQuery) { private void buildUI(String initialQuery) {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12)); mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Top Query bar // Top Query bar
@@ -62,7 +60,6 @@ public class HostDirectoryDialog extends JDialog {
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel envLabel = new JLabel("System:"); JLabel envLabel = new JLabel("System:");
envLabel.setForeground(fg);
envLabel.setFont(font); envLabel.setFont(font);
topPanel.add(envLabel, gbc); topPanel.add(envLabel, gbc);
@@ -70,33 +67,25 @@ public class HostDirectoryDialog extends JDialog {
hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS}); hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS});
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS); if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO); else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
hostTypeCombo.setBackground(new Color(50, 50, 50)); ThemeManager.styleComboBox(hostTypeCombo);
hostTypeCombo.setForeground(fg);
topPanel.add(hostTypeCombo, gbc); topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2; gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:"); JLabel queryLabel = new JLabel("Query Pattern / Text:");
queryLabel.setForeground(fg);
queryLabel.setFont(font); queryLabel.setFont(font);
topPanel.add(queryLabel, gbc); topPanel.add(queryLabel, gbc);
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setBackground(new Color(50, 50, 50));
queryField.setForeground(fg);
queryField.setCaretColor(fg);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
queryField.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(queryField);
new LineBorder(new Color(70, 70, 70)),
new EmptyBorder(3, 5, 3, 5)));
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
gbc.gridx = 4; gbc.gridx = 4;
gbc.weightx = 0; gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse"); JButton parseBtn = new JButton("Query / Parse");
parseBtn.setBackground(new Color(55, 55, 55)); ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
parseBtn.setForeground(fg);
parseBtn.setFont(font); parseBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery()); parseBtn.addActionListener(e -> runQuery());
topPanel.add(parseBtn, gbc); topPanel.add(parseBtn, gbc);
@@ -106,18 +95,12 @@ public class HostDirectoryDialog extends JDialog {
// Table // Table
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel();
table = new JTable(tableModel); table = new JTable(tableModel);
table.setBackground(new Color(45, 45, 45)); ThemeManager.styleTable(table);
table.setForeground(fg);
table.setSelectionBackground(new Color(75, 110, 175));
table.setSelectionForeground(Color.WHITE);
table.setGridColor(new Color(65, 65, 65));
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(20);
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(fg);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
table.addMouseListener(new MouseAdapter() { table.addMouseListener(new MouseAdapter() {
@@ -130,8 +113,7 @@ public class HostDirectoryDialog extends JDialog {
}); });
JScrollPane scrollPane = new JScrollPane(table); JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(new Color(65, 65, 65))); ThemeManager.styleScrollPane(scrollPane);
scrollPane.getViewport().setBackground(new Color(45, 45, 45));
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom // Bottom
@@ -139,7 +121,7 @@ public class HostDirectoryDialog extends JDialog {
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(new Color(170, 170, 170)); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
@@ -147,20 +129,17 @@ public class HostDirectoryDialog extends JDialog {
btnPanel.setOpaque(false); btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen"); JButton pasteScreenBtn = new JButton("Parse Current Screen");
pasteScreenBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteScreenBtn.setForeground(fg);
pasteScreenBtn.setFont(font); pasteScreenBtn.setFont(font);
pasteScreenBtn.addActionListener(e -> parseCurrentScreen()); pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton selectBtn = new JButton("Select Dataset"); JButton selectBtn = new JButton("Select Dataset");
selectBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
selectBtn.setForeground(Color.WHITE);
selectBtn.setFont(font); selectBtn.setFont(font);
selectBtn.addActionListener(e -> onConfirmSelection()); selectBtn.addActionListener(e -> onConfirmSelection());
JButton cancelBtn = new JButton("Cancel"); JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setForeground(fg);
cancelBtn.setFont(font); cancelBtn.setFont(font);
cancelBtn.addActionListener(e -> { cancelBtn.addActionListener(e -> {
confirmed = false; confirmed = false;
@@ -176,6 +155,7 @@ public class HostDirectoryDialog extends JDialog {
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Initial setup // Initial setup
setupTableColumns(); setupTableColumns();
@@ -45,10 +45,6 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool Display // Spool Display
private JTextArea spoolArea; private JTextArea spoolArea;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) { public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false); super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent; this.parent = parent;
@@ -65,14 +61,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(DARK_BG);
// Top: Configuration Panel // Top: Configuration Panel
JPanel topPanel = new JPanel(new GridBagLayout()); JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
topPanel.setBorder(BorderFactory.createTitledBorder( topPanel.setBorder(ThemeManager.createTitledBorder("Printer Session Configuration"));
new LineBorder(new Color(65, 65, 65)), "Printer Session Configuration"));
((TitledBorder) topPanel.getBorder()).setTitleColor(DARK_FG);
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 3, 4); gbc.insets = new Insets(3, 4, 3, 4);
@@ -83,60 +76,59 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Host / Port // Host / Port
gbc.gridx = 0; gbc.gridy = 0; gbc.gridx = 0; gbc.gridy = 0;
JLabel hLbl = new JLabel("Host:"); JLabel hLbl = new JLabel("Host:");
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont); hLbl.setFont(labelFont);
topPanel.add(hLbl, gbc); topPanel.add(hLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridx = 1; gbc.weightx = 1.0;
hostField = createDarkField(config.getHost() != null ? config.getHost() : "localhost", 14); hostField = createField(config.getHost() != null ? config.getHost() : "localhost", 14);
topPanel.add(hostField, gbc); topPanel.add(hostField, gbc);
gbc.gridx = 2; gbc.weightx = 0; gbc.gridx = 2; gbc.weightx = 0;
JLabel pLbl = new JLabel("Port:"); JLabel pLbl = new JLabel("Port:");
pLbl.setForeground(DARK_FG); pLbl.setFont(labelFont); pLbl.setFont(labelFont);
topPanel.add(pLbl, gbc); topPanel.add(pLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5; gbc.gridx = 3; gbc.weightx = 0.5;
portField = createDarkField(String.valueOf(config.getPort()), 5); portField = createField(String.valueOf(config.getPort()), 5);
topPanel.add(portField, gbc); topPanel.add(portField, gbc);
// Printer LU / Display LU // Printer LU / Display LU
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0; gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
JLabel pluLbl = new JLabel("Printer LU:"); JLabel pluLbl = new JLabel("Printer LU:");
pluLbl.setForeground(DARK_FG); pluLbl.setFont(labelFont); pluLbl.setFont(labelFont);
topPanel.add(pluLbl, gbc); topPanel.add(pluLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridx = 1; gbc.weightx = 1.0;
printerLuField = createDarkField("", 10); printerLuField = createField("", 10);
topPanel.add(printerLuField, gbc); topPanel.add(printerLuField, gbc);
gbc.gridx = 2; gbc.weightx = 0; gbc.gridx = 2; gbc.weightx = 0;
JLabel assocLbl = new JLabel("Assoc LU:"); JLabel assocLbl = new JLabel("Assoc LU:");
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont); assocLbl.setFont(labelFont);
topPanel.add(assocLbl, gbc); topPanel.add(assocLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5; gbc.gridx = 3; gbc.weightx = 0.5;
displayLuField = createDarkField("", 10); displayLuField = createField("", 10);
topPanel.add(displayLuField, gbc); topPanel.add(displayLuField, gbc);
// CodePage & TLS // CodePage & TLS
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0; gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
JLabel cpLbl = new JLabel("CodePage:"); JLabel cpLbl = new JLabel("CodePage:");
cpLbl.setForeground(DARK_FG); cpLbl.setFont(labelFont); cpLbl.setFont(labelFont);
topPanel.add(cpLbl, gbc); topPanel.add(cpLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridx = 1; gbc.weightx = 1.0;
codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"}); codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"});
codePageCombo.setBackground(DARK_FIELD_BG); ThemeManager.styleComboBox(codePageCombo);
codePageCombo.setForeground(DARK_FG);
topPanel.add(codePageCombo, gbc); topPanel.add(codePageCombo, gbc);
gbc.gridx = 2; gbc.gridwidth = 2; gbc.gridx = 2; gbc.gridwidth = 2;
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
tlsPanel.setOpaque(false); tlsPanel.setOpaque(false);
tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls()); tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls());
tlsCheck.setOpaque(false); tlsCheck.setForeground(DARK_FG); tlsCheck.setFocusPainted(false); ThemeManager.styleCheckBox(tlsCheck);
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert()); verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
verifyCertCheck.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false); ThemeManager.styleCheckBox(verifyCertCheck);
tlsPanel.add(tlsCheck); tlsPanel.add(tlsCheck);
tlsPanel.add(verifyCertCheck); tlsPanel.add(verifyCertCheck);
topPanel.add(tlsPanel, gbc); topPanel.add(tlsPanel, gbc);
@@ -144,23 +136,22 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Destination Type & Target // Destination Type & Target
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1; gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
JLabel destLbl = new JLabel("Destination:"); JLabel destLbl = new JLabel("Destination:");
destLbl.setForeground(DARK_FG); destLbl.setFont(labelFont); destLbl.setFont(labelFont);
topPanel.add(destLbl, gbc); topPanel.add(destLbl, gbc);
gbc.gridx = 1; gbc.gridx = 1;
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values()); destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY); destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
destinationCombo.setBackground(DARK_FIELD_BG); ThemeManager.styleComboBox(destinationCombo);
destinationCombo.setForeground(DARK_FG);
topPanel.add(destinationCombo, gbc); topPanel.add(destinationCombo, gbc);
gbc.gridx = 2; gbc.gridx = 2;
JLabel tgtLbl = new JLabel("Target Path:"); JLabel tgtLbl = new JLabel("Target Path:");
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont); tgtLbl.setFont(labelFont);
topPanel.add(tgtLbl, gbc); topPanel.add(tgtLbl, gbc);
gbc.gridx = 3; gbc.gridx = 3;
targetField = createDarkField("printer_output.txt", 12); targetField = createField("printer_output.txt", 12);
topPanel.add(targetField, gbc); topPanel.add(targetField, gbc);
// Connect / Disconnect Buttons // Connect / Disconnect Buttons
@@ -169,13 +160,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
connBtnPan.setOpaque(false); connBtnPan.setOpaque(false);
connectBtn = new JButton("Start Printer Session"); connectBtn = new JButton("Start Printer Session");
connectBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.setForeground(Color.WHITE);
connectBtn.addActionListener(e -> startPrinterSession()); connectBtn.addActionListener(e -> startPrinterSession());
disconnectBtn = new JButton("Stop Session"); disconnectBtn = new JButton("Stop Session");
disconnectBtn.setBackground(new Color(120, 50, 50)); ThemeManager.styleButton(disconnectBtn, ThemeManager.ButtonVariant.DANGER);
disconnectBtn.setForeground(Color.WHITE);
disconnectBtn.setEnabled(false); disconnectBtn.setEnabled(false);
disconnectBtn.addActionListener(e -> stopPrinterSession()); disconnectBtn.addActionListener(e -> stopPrinterSession());
@@ -188,9 +177,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Center: Spool Area & Status // Center: Spool Area & Status
JPanel centerPanel = new JPanel(new BorderLayout(6, 6)); JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
centerPanel.setOpaque(false); centerPanel.setOpaque(false);
centerPanel.setBorder(BorderFactory.createTitledBorder( centerPanel.setBorder(ThemeManager.createTitledBorder("Printer Spool & Status"));
new LineBorder(new Color(65, 65, 65)), "Printer Spool & Status"));
((TitledBorder) centerPanel.getBorder()).setTitleColor(DARK_FG);
// Status Header // Status Header
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0)); JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
@@ -198,19 +185,16 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4)); statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
statusLabel = new JLabel("Status: Disconnected"); statusLabel = new JLabel("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180)); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont); statusLabel.setFont(labelFont);
sessionTypeLabel = new JLabel("Session: -"); sessionTypeLabel = new JLabel("Session: -");
sessionTypeLabel.setForeground(DARK_FG);
sessionTypeLabel.setFont(labelFont); sessionTypeLabel.setFont(labelFont);
pagesLabel = new JLabel("Pages: 0"); pagesLabel = new JLabel("Pages: 0");
pagesLabel.setForeground(DARK_FG);
pagesLabel.setFont(labelFont); pagesLabel.setFont(labelFont);
bytesLabel = new JLabel("Bytes: 0"); bytesLabel = new JLabel("Bytes: 0");
bytesLabel.setForeground(DARK_FG);
bytesLabel.setFont(labelFont); bytesLabel.setFont(labelFont);
statusHeader.add(statusLabel); statusHeader.add(statusLabel);
@@ -221,14 +205,12 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area // Spool text area
spoolArea = new JTextArea(); spoolArea = new JTextArea();
spoolArea.setBackground(new Color(25, 25, 25));
spoolArea.setForeground(new Color(100, 255, 100));
spoolArea.setCaretColor(Color.WHITE);
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
spoolArea.setEditable(false); spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea);
JScrollPane spoolScroll = new JScrollPane(spoolArea); JScrollPane spoolScroll = new JScrollPane(spoolArea);
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60))); ThemeManager.styleScrollPane(spoolScroll);
centerPanel.add(spoolScroll, BorderLayout.CENTER); centerPanel.add(spoolScroll, BorderLayout.CENTER);
mainPanel.add(centerPanel, BorderLayout.CENTER); mainPanel.add(centerPanel, BorderLayout.CENTER);
@@ -238,8 +220,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
JButton clearSpoolBtn = new JButton("Clear Spool"); JButton clearSpoolBtn = new JButton("Clear Spool");
clearSpoolBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(clearSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
clearSpoolBtn.setForeground(DARK_FG);
clearSpoolBtn.addActionListener(e -> { clearSpoolBtn.addActionListener(e -> {
spoolArea.setText(""); spoolArea.setText("");
if (printerSession != null && printerSession.getPD() != null) { if (printerSession != null && printerSession.getPD() != null) {
@@ -250,18 +231,15 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
}); });
JButton saveSpoolBtn = new JButton("Save Spool As..."); JButton saveSpoolBtn = new JButton("Save Spool As...");
saveSpoolBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(saveSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
saveSpoolBtn.setForeground(DARK_FG);
saveSpoolBtn.addActionListener(e -> saveSpool()); saveSpoolBtn.addActionListener(e -> saveSpool());
JButton printSpoolBtn = new JButton("Print Spool..."); JButton printSpoolBtn = new JButton("Print Spool...");
printSpoolBtn.setBackground(new Color(50, 100, 160)); ThemeManager.styleButton(printSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
printSpoolBtn.setForeground(Color.WHITE);
printSpoolBtn.addActionListener(e -> printSpool()); printSpoolBtn.addActionListener(e -> printSpool());
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
bottomPanel.add(clearSpoolBtn); bottomPanel.add(clearSpoolBtn);
@@ -273,17 +251,13 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
} }
private JTextField createDarkField(String text, int cols) { private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols); JTextField tf = new JTextField(text, cols);
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
tf.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(tf);
new LineBorder(new Color(65, 65, 65)),
new EmptyBorder(2, 4, 2, 4)));
return tf; return tf;
} }
@@ -34,9 +34,7 @@ public class ScriptDialog extends JDialog {
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Header // Header
@@ -44,31 +42,24 @@ public class ScriptDialog extends JDialog {
topPanel.setOpaque(false); topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>" JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>"
+ "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>"); + "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>");
descLabel.setForeground(new Color(180, 180, 180));
descLabel.setFont(labelFont); descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER); topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH); mainPanel.add(topPanel, BorderLayout.NORTH);
// Script area // Script area
scriptArea = new JTextArea(); scriptArea = new JTextArea();
scriptArea.setBackground(new Color(25, 25, 25));
scriptArea.setForeground(new Color(50, 205, 50));
scriptArea.setCaretColor(Color.WHITE);
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
scriptArea.setLineWrap(true); scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false); scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea);
JScrollPane scrollPane = new JScrollPane(scriptArea); JScrollPane scrollPane = new JScrollPane(scriptArea);
scrollPane.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleScrollPane(scrollPane);
new LineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 2, 2, 2)));
// Mnemonic helper buttons // Mnemonic helper buttons
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4)); JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
tokenPanel.setOpaque(false); tokenPanel.setOpaque(false);
tokenPanel.setBorder(BorderFactory.createTitledBorder( tokenPanel.setBorder(ThemeManager.createTitledBorder("Insert Keystroke Token"));
new LineBorder(new Color(60, 60, 60)), "Insert Keystroke Token"));
((TitledBorder) tokenPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
String[] tokens = { String[] tokens = {
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]", "[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
@@ -84,8 +75,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) { for (String token : tokens) {
JButton btn = new JButton(token); JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
btn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setForeground(fg);
btn.setFocusable(false); btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4)); btn.setMargin(new Insets(2, 4, 2, 4));
btn.addActionListener(e -> insertToken(token)); btn.addActionListener(e -> insertToken(token));
@@ -103,7 +93,7 @@ public class ScriptDialog extends JDialog {
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
statusLabel = new JLabel("Ready"); statusLabel = new JLabel("Ready");
statusLabel.setForeground(new Color(160, 160, 160)); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
@@ -111,26 +101,22 @@ public class ScriptDialog extends JDialog {
buttonPanel.setOpaque(false); buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load Script..."); JButton loadBtn = new JButton("Load Script...");
loadBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setForeground(fg);
loadBtn.setFont(labelFont); loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadScript()); loadBtn.addActionListener(e -> loadScript());
JButton saveBtn = new JButton("Save Script..."); JButton saveBtn = new JButton("Save Script...");
saveBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setForeground(fg);
saveBtn.setFont(labelFont); saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveScript()); saveBtn.addActionListener(e -> saveScript());
JButton runBtn = new JButton("Execute"); JButton runBtn = new JButton("Execute");
runBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(runBtn, ThemeManager.ButtonVariant.PRIMARY);
runBtn.setForeground(Color.WHITE);
runBtn.setFont(labelFont); runBtn.setFont(labelFont);
runBtn.addActionListener(e -> executeScript()); runBtn.addActionListener(e -> executeScript());
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont); closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
@@ -144,6 +130,7 @@ public class ScriptDialog extends JDialog {
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
} }
private void insertToken(String token) { private void insertToken(String token) {
@@ -19,16 +19,8 @@ public class SettingsDialog extends JDialog {
private final J3270App parentApp; private final J3270App parentApp;
// Dark theme colors
private static final Color DARK_BG = new Color(43, 43, 43);
private static final Color DARK_BG_LIGHTER = new Color(55, 55, 55);
private static final Color DARK_FG = new Color(224, 224, 224);
private static final Color DARK_BORDER = new Color(70, 70, 70);
private static final Color DARK_SELECTION = new Color(75, 110, 175);
private static final Color DARK_BUTTON_BG = new Color(60, 63, 65);
private static final Color DARK_FIELD_BG = new Color(50, 50, 50);
// Appearance tab // Appearance tab
private JComboBox<UITheme> uiThemeBox;
private JComboBox<String> fontBox; private JComboBox<String> fontBox;
private JSpinner fontSizeSpinner; private JSpinner fontSizeSpinner;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox; private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
@@ -51,22 +43,30 @@ public class SettingsDialog extends JDialog {
this.parentApp = parent; this.parentApp = parent;
initComponents(); initComponents();
setSize(550, 450); setSize(560, 480);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
private void initComponents() { private void initComponents() {
JTabbedPane tabbedPane = new JTabbedPane(); JTabbedPane tabbedPane = new JTabbedPane();
ThemeManager.styleTabbedPane(tabbedPane);
tabbedPane.addTab("Appearance", createAppearancePanel()); tabbedPane.addTab("Appearance", createAppearancePanel());
tabbedPane.addTab("Behavior", createBehaviorPanel()); tabbedPane.addTab("Behavior", createBehaviorPanel());
tabbedPane.addTab("Advanced", createAdvancedPanel()); tabbedPane.addTab("Advanced", createAdvancedPanel());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
JButton btnExport = new JButton("Export Config..."); JButton btnExport = new JButton("Export Config...");
JButton btnOk = new JButton("OK"); ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
JButton btnApply = new JButton("Apply"); JButton btnApply = new JButton("Apply");
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
JButton btnCancel = new JButton("Cancel"); JButton btnCancel = new JButton("Cancel");
ThemeManager.styleButton(btnCancel, ThemeManager.ButtonVariant.CANCEL);
JButton btnOk = new JButton("OK");
ThemeManager.styleButton(btnOk, ThemeManager.ButtonVariant.PRIMARY);
btnExport.addActionListener((ActionEvent e) -> { btnExport.addActionListener((ActionEvent e) -> {
exportConfig(); exportConfig();
@@ -96,132 +96,11 @@ public class SettingsDialog extends JDialog {
getContentPane().add(tabbedPane, BorderLayout.CENTER); getContentPane().add(tabbedPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH); getContentPane().add(buttonPanel, BorderLayout.SOUTH);
// Apply dark theme to all components for cross-platform readability ThemeManager.applyThemeToWindow(this);
applyDarkTheme(getContentPane());
applyDarkTheme(tabbedPane);
applyDarkTheme(buttonPanel);
getContentPane().setBackground(DARK_BG);
} }
// ========== Dark Theme Utility ==========
/**
* Recursively apply dark theme to a component and all its children.
* Ensures the Settings dialog is readable on Windows, Linux, and macOS.
*/
private void applyDarkTheme(Component comp) { private void applyDarkTheme(Component comp) {
if (comp instanceof JTabbedPane) { ThemeManager.applyTheme(comp);
JTabbedPane tp = (JTabbedPane) comp;
tp.setBackground(DARK_BG);
tp.setForeground(DARK_FG);
for (int i = 0; i < tp.getTabCount(); i++) {
applyDarkTheme(tp.getComponentAt(i));
}
return;
}
if (comp instanceof JTable) {
JTable table = (JTable) comp;
table.setBackground(DARK_FIELD_BG);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
JTableHeader header = table.getTableHeader();
if (header != null) {
header.setBackground(DARK_BG_LIGHTER);
header.setForeground(DARK_FG);
DefaultTableCellRenderer headerRenderer = new DefaultTableCellRenderer();
headerRenderer.setBackground(DARK_BG_LIGHTER);
headerRenderer.setForeground(DARK_FG);
header.setDefaultRenderer(headerRenderer);
}
return;
}
if (comp instanceof JScrollPane) {
JScrollPane sp = (JScrollPane) comp;
sp.setBackground(DARK_BG);
sp.getViewport().setBackground(DARK_FIELD_BG);
applyDarkTheme(sp.getViewport().getView());
return;
}
if (comp instanceof JButton) {
JButton btn = (JButton) comp;
btn.setBackground(DARK_BUTTON_BG);
btn.setForeground(DARK_FG);
btn.setFocusPainted(false);
btn.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(DARK_BORDER, 1),
BorderFactory.createEmptyBorder(3, 10, 3, 10)));
btn.setOpaque(true);
return;
}
if (comp instanceof JComboBox) {
JComboBox<?> cb = (JComboBox<?>) comp;
cb.setBackground(DARK_FIELD_BG);
cb.setForeground(DARK_FG);
return;
}
if (comp instanceof JSpinner) {
JSpinner sp = (JSpinner) comp;
sp.setBackground(DARK_FIELD_BG);
sp.setForeground(DARK_FG);
JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
}
return;
}
if (comp instanceof JTextField) {
JTextField tf = (JTextField) comp;
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
return;
}
if (comp instanceof JCheckBox) {
JCheckBox cb = (JCheckBox) comp;
cb.setBackground(DARK_BG);
cb.setForeground(DARK_FG);
return;
}
if (comp instanceof JLabel) {
comp.setForeground(DARK_FG);
return;
}
// Generic panel / container
// Skip color swatch panels their background IS the color
if (comp instanceof JPanel && "colorSwatch".equals(comp.getName())) {
return;
}
comp.setBackground(DARK_BG);
comp.setForeground(DARK_FG);
if (comp instanceof JPanel) {
JPanel panel = (JPanel) comp;
// Style titled borders
if (panel.getBorder() instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) panel.getBorder();
tb.setTitleColor(DARK_FG);
}
}
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyDarkTheme(child);
}
}
} }
// ========== Tab Panels ========== // ========== Tab Panels ==========
@@ -232,9 +111,22 @@ public class SettingsDialog extends JDialog {
gbc.insets = new Insets(10, 10, 10, 10); gbc.insets = new Insets(10, 10, 10, 10);
gbc.anchor = GridBagConstraints.WEST; gbc.anchor = GridBagConstraints.WEST;
// Font family // UI Theme
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = 0;
JLabel themeLabel = new JLabel("Java UI Theme:");
panel.add(themeLabel, gbc);
uiThemeBox = new JComboBox<>(UITheme.values());
uiThemeBox.setSelectedItem(Settings.getJavaUiTheme());
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(uiThemeBox, gbc);
// Font family
gbc.gridx = 0;
gbc.gridy = 1;
gbc.fill = GridBagConstraints.NONE;
JLabel fontLabel = new JLabel("Terminal Font:"); JLabel fontLabel = new JLabel("Terminal Font:");
panel.add(fontLabel, gbc); panel.add(fontLabel, gbc);
@@ -254,7 +146,7 @@ public class SettingsDialog extends JDialog {
// Font size // Font size
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = 2;
gbc.fill = GridBagConstraints.NONE; gbc.fill = GridBagConstraints.NONE;
JLabel sizeLabel = new JLabel("Font Size:"); JLabel sizeLabel = new JLabel("Font Size:");
panel.add(sizeLabel, gbc); panel.add(sizeLabel, gbc);
@@ -267,7 +159,7 @@ public class SettingsDialog extends JDialog {
// Graphics Mode // Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 2; gbc.gridy = 3;
gbc.fill = GridBagConstraints.NONE; gbc.fill = GridBagConstraints.NONE;
JLabel graphicsLabel = new JLabel("Graphics Mode:"); JLabel graphicsLabel = new JLabel("Graphics Mode:");
panel.add(graphicsLabel, gbc); panel.add(graphicsLabel, gbc);
@@ -279,7 +171,7 @@ public class SettingsDialog extends JDialog {
panel.add(graphicsModeBox, gbc); panel.add(graphicsModeBox, gbc);
// Fill remaining space // Fill remaining space
gbc.gridy = 3; gbc.gridy = 4;
gbc.weighty = 1.0; gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc); panel.add(Box.createGlue(), gbc);
@@ -429,6 +321,7 @@ public class SettingsDialog extends JDialog {
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnResetColors = new JButton("Reset to Defaults"); JButton btnResetColors = new JButton("Reset to Defaults");
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
btnResetColors.addActionListener(e -> { btnResetColors.addActionListener(e -> {
for (int i=0; i<16; i++) { for (int i=0; i<16; i++) {
tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i]; tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i];
@@ -436,8 +329,6 @@ public class SettingsDialog extends JDialog {
for (int i=0; i<monoKeys.length; i++) { for (int i=0; i<monoKeys.length; i++) {
tempMonoColors.put(monoKeys[i], monoDefs[i]); tempMonoColors.put(monoKeys[i], monoDefs[i]);
} }
// Repaint container implicitly handled if we trigger a UI update,
// but for simplicity user can just close/reopen or have it refresh on save
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save."); JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
}); });
resetPanel.add(btnResetColors); resetPanel.add(btnResetColors);
@@ -498,6 +389,7 @@ public class SettingsDialog extends JDialog {
} }
JTable table = new JTable(keymapModel); JTable table = new JTable(keymapModel);
ThemeManager.styleTable(table);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setFillsViewportHeight(true); table.setFillsViewportHeight(true);
@@ -510,9 +402,8 @@ public class SettingsDialog extends JDialog {
captureDialog.setSize(320, 100); captureDialog.setSize(320, 100);
captureDialog.setLocationRelativeTo(this); captureDialog.setLocationRelativeTo(this);
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER); JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
lbl.setForeground(DARK_FG);
captureDialog.getContentPane().setBackground(DARK_BG);
captureDialog.add(lbl); captureDialog.add(lbl);
ThemeManager.applyThemeToWindow(captureDialog);
captureDialog.addKeyListener(new KeyAdapter() { captureDialog.addKeyListener(new KeyAdapter() {
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
@@ -532,6 +423,7 @@ public class SettingsDialog extends JDialog {
// Replace Binding sets the action to exactly one new key // Replace Binding sets the action to exactly one new key
JButton btnReplace = new JButton("Replace Binding"); JButton btnReplace = new JButton("Replace Binding");
ThemeManager.styleButton(btnReplace, ThemeManager.ButtonVariant.DEFAULT);
btnReplace.addActionListener(e -> { btnReplace.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -545,6 +437,7 @@ public class SettingsDialog extends JDialog {
// Add Binding appends an additional key to the existing binding(s) // Add Binding appends an additional key to the existing binding(s)
JButton btnAdd = new JButton("Add Binding"); JButton btnAdd = new JButton("Add Binding");
ThemeManager.styleButton(btnAdd, ThemeManager.ButtonVariant.DEFAULT);
btnAdd.addActionListener(e -> { btnAdd.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -565,6 +458,7 @@ public class SettingsDialog extends JDialog {
// Remove Last removes the last comma-separated binding entry // Remove Last removes the last comma-separated binding entry
JButton btnRemoveLast = new JButton("Remove Last"); JButton btnRemoveLast = new JButton("Remove Last");
ThemeManager.styleButton(btnRemoveLast, ThemeManager.ButtonVariant.DEFAULT);
btnRemoveLast.addActionListener(e -> { btnRemoveLast.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -585,6 +479,7 @@ public class SettingsDialog extends JDialog {
// Unbind All clears all bindings for the action // Unbind All clears all bindings for the action
JButton btnUnbind = new JButton("Unbind All"); JButton btnUnbind = new JButton("Unbind All");
ThemeManager.styleButton(btnUnbind, ThemeManager.ButtonVariant.DEFAULT);
btnUnbind.addActionListener(e -> { btnUnbind.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -596,6 +491,7 @@ public class SettingsDialog extends JDialog {
// Reset Keymaps restore all defaults // Reset Keymaps restore all defaults
JButton btnReset = new JButton("Reset All"); JButton btnReset = new JButton("Reset All");
ThemeManager.styleButton(btnReset, ThemeManager.ButtonVariant.DEFAULT);
btnReset.addActionListener(e -> { btnReset.addActionListener(e -> {
for (int row = 0; row < keymapModel.getRowCount(); row++) { for (int row = 0; row < keymapModel.getRowCount(); row++) {
String action = (String) keymapModel.getValueAt(row, 0); String action = (String) keymapModel.getValueAt(row, 0);
@@ -628,6 +524,12 @@ public class SettingsDialog extends JDialog {
private boolean applySettings() { private boolean applySettings() {
try { try {
// Apply Appearance // Apply Appearance
UITheme selectedTheme = (UITheme) uiThemeBox.getSelectedItem();
if (selectedTheme != null) {
Settings.setJavaUiTheme(selectedTheme);
ThemeManager.setTheme(selectedTheme);
}
String fontFam = (String) fontBox.getSelectedItem(); String fontFam = (String) fontBox.getSelectedItem();
if (fontFam != null) { if (fontFam != null) {
Settings.setFontFamily(fontFam); Settings.setFontFamily(fontFam);
@@ -668,6 +570,7 @@ public class SettingsDialog extends JDialog {
} }
parentApp.getTerminalPanel().reloadSettings(); parentApp.getTerminalPanel().reloadSettings();
ThemeManager.applyThemeToWindow(this);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
@@ -26,29 +26,22 @@ public class StatusBar extends JPanel {
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
// OIA colors
private static final Color OIA_BG = new Color(20, 20, 20);
private static final Color OIA_FG = new Color(50, 205, 50);
private static final Color OIA_DIM = new Color(80, 80, 80);
private static final Color OIA_ALERT = new Color(255, 80, 80);
private static final Color OIA_WARN = new Color(255, 200, 80);
public StatusBar() { public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(OIA_BG); setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40))); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
setPreferredSize(new Dimension(800, 22)); setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, OIA_FG); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
luName = createLabel("", oiaFont, OIA_FG); luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
lockStatus = createLabel("", oiaFont, OIA_ALERT); lockStatus = createLabel("", oiaFont, ThemeManager.getOiaFgAlert());
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM); fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
codePageInfo = createLabel("", oiaFont, OIA_DIM); codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
modelInfo = createLabel("", oiaFont, OIA_DIM); modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG); cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
add(Box.createHorizontalStrut(6)); add(Box.createHorizontalStrut(6));
add(connectionStatus); add(connectionStatus);
@@ -82,10 +75,17 @@ public class StatusBar extends JPanel {
updateStatus(); updateStatus();
} }
public void applyTheme(UITheme theme) {
setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
updateStatus();
}
public void updateStatus() { public void updateStatus() {
UITheme theme = ThemeManager.getTheme();
if (client == null) { if (client == null) {
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
tlsStatus.setText(""); tlsStatus.setText("");
luName.setText(""); luName.setText("");
lockStatus.setText(""); lockStatus.setText("");
@@ -101,38 +101,38 @@ public class StatusBar extends JPanel {
switch (state) { switch (state) {
case NOT_CONNECTED: case NOT_CONNECTED:
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break; break;
case TCP_PENDING: case TCP_PENDING:
case TELNET_PENDING: case TELNET_PENDING:
connectionStatus.setText("Connecting..."); connectionStatus.setText("Connecting...");
connectionStatus.setForeground(OIA_ALERT); connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
break; break;
case CONNECTED_3270: case CONNECTED_3270:
connectionStatus.setText("TN3270"); connectionStatus.setText("TN3270");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_TN3270E: case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E"); connectionStatus.setText("TN3270E");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_SSCP: case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU"); connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_NVT: case CONNECTED_NVT:
case CONNECTED_NVT_CHAR: case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT: case CONNECTED_E_NVT:
connectionStatus.setText("NVT"); connectionStatus.setText("NVT");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_UNBOUND: case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound"); connectionStatus.setText("Unbound");
connectionStatus.setForeground(OIA_WARN); connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
break; break;
default: default:
connectionStatus.setText(state.name()); connectionStatus.setText(state.name());
connectionStatus.setForeground(OIA_DIM); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break; break;
} }
@@ -144,11 +144,11 @@ public class StatusBar extends JPanel {
String protocol = session != null ? session.getProtocol() : "TLS"; String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) { if (verified) {
tlsStatus.setText("🔒 TLS"); tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(OIA_FG); tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else { } else {
tlsStatus.setText("🔓 TLS (Unverified)"); tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(new Color(255, 180, 80)); tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
} }
} else { } else {
@@ -164,6 +164,7 @@ public class StatusBar extends JPanel {
lu = "LU:" + client.getConfig().getLuName(); lu = "LU:" + client.getConfig().getLuName();
} }
luName.setText(lu); luName.setText(lu);
luName.setForeground(ThemeManager.getOiaFgNormal(theme));
// Lock / Inhibit status // Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited(); int inhibit = client.getOIA().getInputInhibited();
@@ -191,10 +192,10 @@ public class StatusBar extends JPanel {
lockStatus.setText("X LOCKED"); lockStatus.setText("X LOCKED");
break; break;
} }
lockStatus.setForeground(OIA_ALERT); lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
} else if (client.getInputProcessor().isInsertMode()) { } else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT"); lockStatus.setText("INSERT");
lockStatus.setForeground(OIA_FG); lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
} else { } else {
lockStatus.setText(""); lockStatus.setText("");
} }
@@ -203,10 +204,10 @@ public class StatusBar extends JPanel {
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) { if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM"); fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(OIA_WARN); fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else { } else {
fieldTypeStatus.setText("ALPHA"); fieldTypeStatus.setText("ALPHA");
fieldTypeStatus.setForeground(OIA_DIM); fieldTypeStatus.setForeground(ThemeManager.getOiaFgDim(theme));
} }
} else { } else {
fieldTypeStatus.setText(""); fieldTypeStatus.setText("");
@@ -215,6 +216,7 @@ public class StatusBar extends JPanel {
// Active Code Page // Active Code Page
String cp = client.getCodePage(); String cp = client.getCodePage();
codePageInfo.setText(cp != null ? "CP" + cp : ""); codePageInfo.setText(cp != null ? "CP" + cp : "");
codePageInfo.setForeground(ThemeManager.getOiaFgDim(theme));
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp); codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
// Model & Dimensions info // Model & Dimensions info
@@ -222,11 +224,13 @@ public class StatusBar extends JPanel {
int rows = sb.getDisplayRows(); int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols(); int cols = sb.getDisplayCols();
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]"); modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
// Cursor position and buffer address // Cursor position and buffer address
int curAddr = sb.getCursorAddress(); int curAddr = sb.getCursorAddress();
int row = sb.getCursorRow() + 1; int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1; int col = sb.getCursorCol() + 1;
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr)); cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
} }
} }
@@ -434,6 +434,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
public void pasteClipboard() { public void pasteClipboard() {
if (client == null || !client.getConnectionState().isFullSession()) return; if (client == null || !client.getConnectionState().isFullSession()) return;
if (client.getConnectionState().isNvt()) {
try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor);
if (text != null && !text.isEmpty()) {
client.sendNVTString(text);
}
} catch (Exception ignored) {}
return;
}
try { try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard() String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor); .getData(DataFlavor.stringFlavor);
@@ -481,24 +491,29 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void showContextMenu(MouseEvent e) { private void showContextMenu(MouseEvent e) {
JPopupMenu popup = new JPopupMenu(); JPopupMenu popup = new JPopupMenu();
ThemeManager.stylePopupMenu(popup);
JMenuItem copyItem = new JMenuItem("Copy"); JMenuItem copyItem = new JMenuItem("Copy");
ThemeManager.styleMenuItem(copyItem);
copyItem.setEnabled(hasSelection()); copyItem.setEnabled(hasSelection());
copyItem.addActionListener(ev -> copySelection()); copyItem.addActionListener(ev -> copySelection());
popup.add(copyItem); popup.add(copyItem);
JMenuItem pasteItem = new JMenuItem("Paste"); JMenuItem pasteItem = new JMenuItem("Paste");
ThemeManager.styleMenuItem(pasteItem);
pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession()); pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession());
pasteItem.addActionListener(ev -> pasteClipboard()); pasteItem.addActionListener(ev -> pasteClipboard());
popup.add(pasteItem); popup.add(pasteItem);
JMenuItem selectAllItem = new JMenuItem("Select All"); JMenuItem selectAllItem = new JMenuItem("Select All");
ThemeManager.styleMenuItem(selectAllItem);
selectAllItem.addActionListener(ev -> selectAll()); selectAllItem.addActionListener(ev -> selectAll());
popup.add(selectAllItem); popup.add(selectAllItem);
popup.addSeparator(); popup.addSeparator();
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode); JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
ThemeManager.styleMenuItem(blockModeItem);
blockModeItem.addActionListener(ev -> { blockModeItem.addActionListener(ev -> {
blockSelectMode = blockModeItem.isSelected(); blockSelectMode = blockModeItem.isSelected();
haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode); haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
@@ -693,19 +708,63 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
@Override @Override
protected void processKeyEvent(KeyEvent e) { protected void processKeyEvent(KeyEvent e) {
if (e.getID() == KeyEvent.KEY_TYPED) { if (client != null) {
char ch = e.getKeyChar(); ConnectionState state = client.getConnectionState();
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED if (state.isNvt()) {
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) { if (e.getID() == KeyEvent.KEY_TYPED) {
if (client != null) { char ch = e.getKeyChar();
ConnectionState state = client.getConnectionState(); if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) { && !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
try { try {
client.sendNVTChar(ch); client.sendNVTChar(ch);
} catch (Exception ignored) {} } catch (Exception ignored) {}
e.consume(); e.consume();
return; return;
} else if (state.isFullSession()) { } else if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
if (ch > 0 && ch < 0x20) {
try {
client.sendNVTChar(ch);
} catch (Exception ignored) {}
e.consume();
return;
}
}
} else if (e.getID() == KeyEvent.KEY_PRESSED) {
if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
int code = e.getKeyCode();
if (code >= KeyEvent.VK_A && code <= KeyEvent.VK_Z) {
char ctrlChar = (char) (code - KeyEvent.VK_A + 1);
try {
client.sendNVTChar(ctrlChar);
} catch (Exception ignored) {}
e.consume();
return;
} else if (code == KeyEvent.VK_OPEN_BRACKET) { // Ctrl+[ = ESC
try {
client.sendNVTChar('\u001B');
} catch (Exception ignored) {}
e.consume();
return;
} else if (code == KeyEvent.VK_BACK_SLASH) { // Ctrl+\ = FS
try {
client.sendNVTChar('\u001C');
} catch (Exception ignored) {}
e.consume();
return;
} else if (code == KeyEvent.VK_CLOSE_BRACKET) { // Ctrl+] = GS
try {
client.sendNVTChar('\u001D');
} catch (Exception ignored) {}
e.consume();
return;
}
}
}
} else if (e.getID() == KeyEvent.KEY_TYPED) {
char ch = e.getKeyChar();
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
if (state.isFullSession()) {
client.typeCharacter(ch); client.typeCharacter(ch);
refreshScreen(); refreshScreen();
e.consume(); e.consume();
@@ -731,7 +790,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleEnter() { private void handleEnter() {
if (client != null) { if (client != null) {
ConnectionState state = client.getConnectionState(); ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) { if (state.isNvt()) {
try { try {
client.sendNVTString("\r\n"); client.sendNVTString("\r\n");
} catch (Exception ignored) {} } catch (Exception ignored) {}
@@ -744,6 +803,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleReset() { private void handleReset() {
if (client != null) { if (client != null) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTChar('\u001B');
} catch (Exception ignored) {}
return;
}
clearSearchHighlight(); clearSearchHighlight();
clearSelection(); clearSelection();
client.reset(); client.reset();
@@ -753,6 +818,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleTab(boolean shift) { private void handleTab(boolean shift) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
if (shift) {
client.sendNVTString("\u001B[Z");
} else {
client.sendNVTChar('\t');
}
} catch (Exception ignored) {}
return;
}
if (shift) client.backTab(); if (shift) client.backTab();
else client.tab(); else client.tab();
refreshScreen(); refreshScreen();
@@ -761,6 +836,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleCursor(String dir) { private void handleCursor(String dir) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
switch (dir) {
case "up": client.sendNVTString("\u001B[A"); break;
case "down": client.sendNVTString("\u001B[B"); break;
case "left": client.sendNVTString("\u001B[D"); break;
case "right": client.sendNVTString("\u001B[C"); break;
case "home": client.sendNVTString("\u001B[H"); break;
}
} catch (Exception ignored) {}
return;
}
switch (dir) { switch (dir) {
case "up": client.cursorUp(); break; case "up": client.cursorUp(); break;
case "down": client.cursorDown(); break; case "down": client.cursorDown(); break;
@@ -774,11 +861,35 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handlePF(int n) { private void handlePF(int n) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTString(getNvtFunctionKeySequence(n));
} catch (Exception ignored) {}
return;
}
client.sendPF(n); client.sendPF(n);
refreshScreen(); refreshScreen();
} }
} }
private String getNvtFunctionKeySequence(int n) {
switch (n) {
case 1: return "\u001BOP";
case 2: return "\u001BOQ";
case 3: return "\u001BOR";
case 4: return "\u001BOS";
case 5: return "\u001B[15~";
case 6: return "\u001B[17~";
case 7: return "\u001B[18~";
case 8: return "\u001B[19~";
case 9: return "\u001B[20~";
case 10: return "\u001B[21~";
case 11: return "\u001B[23~";
case 12: return "\u001B[24~";
default: return "";
}
}
private void handlePA(int n) { private void handlePA(int n) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
client.sendPA(n); client.sendPA(n);
@@ -788,6 +899,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleEraseEOF() { private void handleEraseEOF() {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTString("\u001B[F");
} catch (Exception ignored) {}
return;
}
client.getInputProcessor().eraseEof(); client.getInputProcessor().eraseEof();
refreshScreen(); refreshScreen();
} }
@@ -795,6 +912,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleDelete() { private void handleDelete() {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTString("\u001B[3~");
} catch (Exception ignored) {}
return;
}
client.getInputProcessor().deleteChar(); client.getInputProcessor().deleteChar();
refreshScreen(); refreshScreen();
} }
@@ -803,7 +926,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleBackspace() { private void handleBackspace() {
if (client != null) { if (client != null) {
ConnectionState state = client.getConnectionState(); ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) { if (state.isNvt()) {
try { try {
client.sendNVTChar('\b'); client.sendNVTChar('\b');
} catch (Exception ignored) {} } catch (Exception ignored) {}
@@ -824,6 +947,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleClear() { private void handleClear() {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTChar('\u000C');
} catch (Exception ignored) {}
return;
}
client.sendClear(); client.sendClear();
refreshScreen(); refreshScreen();
} }
@@ -0,0 +1,955 @@
package haus.nightmare.j3270.ui;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.plaf.basic.BasicButtonUI;
import javax.swing.plaf.basic.BasicMenuBarUI;
import javax.swing.plaf.basic.BasicMenuItemUI;
import javax.swing.plaf.basic.BasicMenuUI;
import javax.swing.plaf.basic.BasicPopupMenuUI;
import javax.swing.plaf.basic.BasicTabbedPaneUI;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Centralized theme manager for j3270 Java desktop UI.
* Provides high-contrast, platform-independent Light and Dark theme palettes,
* custom Swing UI delegates, and recursive styling helpers for all dialogs and menus.
*/
public final class ThemeManager {
private static UITheme currentTheme = UITheme.DARK;
private static final List<Consumer<UITheme>> themeChangeListeners = new ArrayList<>();
private ThemeManager() {}
public static UITheme getTheme() {
return currentTheme;
}
public static boolean isDark() {
return currentTheme == UITheme.DARK;
}
public static boolean isLight() {
return currentTheme == UITheme.LIGHT;
}
public static void setTheme(UITheme theme) {
if (theme == null) theme = UITheme.DARK;
if (currentTheme != theme) {
currentTheme = theme;
applyUIManagerDefaults(theme);
for (Consumer<UITheme> listener : new ArrayList<>(themeChangeListeners)) {
try {
listener.accept(theme);
} catch (Exception ignored) {}
}
}
}
public static void addThemeChangeListener(Consumer<UITheme> listener) {
if (listener != null && !themeChangeListeners.contains(listener)) {
themeChangeListeners.add(listener);
}
}
public static void removeThemeChangeListener(Consumer<UITheme> listener) {
themeChangeListeners.remove(listener);
}
// =========================================================================
// Color Palette Tokens
// =========================================================================
public static Color getBgMain() { return getBgMain(currentTheme); }
public static Color getBgMain(UITheme t) {
return t == UITheme.DARK ? new Color(28, 28, 28) : new Color(242, 242, 244);
}
public static Color getBgPanel() { return getBgPanel(currentTheme); }
public static Color getBgPanel(UITheme t) {
return t == UITheme.DARK ? new Color(36, 36, 36) : new Color(250, 250, 252);
}
public static Color getBgComponent() { return getBgComponent(currentTheme); }
public static Color getBgComponent(UITheme t) {
return t == UITheme.DARK ? new Color(46, 46, 46) : Color.WHITE;
}
public static Color getBgComponentLighter() { return getBgComponentLighter(currentTheme); }
public static Color getBgComponentLighter(UITheme t) {
return t == UITheme.DARK ? new Color(56, 56, 56) : new Color(236, 236, 238);
}
public static Color getFgMain() { return getFgMain(currentTheme); }
public static Color getFgMain(UITheme t) {
return t == UITheme.DARK ? new Color(238, 238, 238) : new Color(24, 24, 24);
}
public static Color getFgMuted() { return getFgMuted(currentTheme); }
public static Color getFgMuted(UITheme t) {
return t == UITheme.DARK ? new Color(175, 175, 175) : new Color(90, 90, 90);
}
public static Color getFgDisabled() { return getFgDisabled(currentTheme); }
public static Color getFgDisabled(UITheme t) {
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(160, 160, 160);
}
public static Color getBorder() { return getBorder(currentTheme); }
public static Color getBorder(UITheme t) {
return t == UITheme.DARK ? new Color(65, 65, 65) : new Color(205, 205, 205);
}
public static Color getBorderColor() { return getBorder(currentTheme); }
public static Color getBorderColor(UITheme t) { return getBorder(t); }
public static Color getBorderFocused() { return getBorderFocused(currentTheme); }
public static Color getBorderFocused(UITheme t) {
return t == UITheme.DARK ? new Color(80, 140, 230) : new Color(50, 115, 220);
}
public static Color getSelectionBg() { return getSelectionBg(currentTheme); }
public static Color getSelectionBg(UITheme t) {
return t == UITheme.DARK ? new Color(55, 105, 180) : new Color(60, 125, 215);
}
public static Color getSelectionFg() { return getSelectionFg(currentTheme); }
public static Color getSelectionFg(UITheme t) {
return Color.WHITE;
}
public static Color getMenuBarBg() { return getMenuBarBg(currentTheme); }
public static Color getMenuBarBg(UITheme t) {
return t == UITheme.DARK ? new Color(26, 26, 26) : new Color(240, 240, 242);
}
public static Color getMenuBarFg() { return getMenuBarFg(currentTheme); }
public static Color getMenuBarFg(UITheme t) {
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(25, 25, 25);
}
public static Color getMenuPopupBg() { return getMenuPopupBg(currentTheme); }
public static Color getMenuPopupBg(UITheme t) {
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
}
public static Color getMenuItemFg() { return getMenuItemFg(currentTheme); }
public static Color getMenuItemFg(UITheme t) {
return t == UITheme.DARK ? new Color(230, 230, 230) : new Color(30, 30, 30);
}
public static Color getMenuItemHoverBg() { return getMenuItemHoverBg(currentTheme); }
public static Color getMenuItemHoverBg(UITheme t) {
return t == UITheme.DARK ? new Color(55, 95, 160) : new Color(220, 232, 250);
}
public static Color getMenuItemHoverFg() { return getMenuItemHoverFg(currentTheme); }
public static Color getMenuItemHoverFg(UITheme t) {
return t == UITheme.DARK ? Color.WHITE : new Color(15, 15, 15);
}
public static Color getTabBg() { return getTabBg(currentTheme); }
public static Color getTabBg(UITheme t) {
return t == UITheme.DARK ? new Color(34, 34, 34) : new Color(230, 230, 232);
}
public static Color getTabFg() { return getTabFg(currentTheme); }
public static Color getTabFg(UITheme t) {
return t == UITheme.DARK ? new Color(185, 185, 185) : new Color(75, 75, 75);
}
public static Color getTabSelectedBg() { return getTabSelectedBg(currentTheme); }
public static Color getTabSelectedBg(UITheme t) {
return t == UITheme.DARK ? new Color(50, 50, 50) : Color.WHITE;
}
public static Color getTabSelectedFg() { return getTabSelectedFg(currentTheme); }
public static Color getTabSelectedFg(UITheme t) {
return t == UITheme.DARK ? Color.WHITE : new Color(20, 20, 20);
}
public static Color getTableHeaderBg() { return getTableHeaderBg(currentTheme); }
public static Color getTableHeaderBg(UITheme t) {
return t == UITheme.DARK ? new Color(46, 46, 46) : new Color(232, 232, 236);
}
public static Color getTableHeaderFg() { return getTableHeaderFg(currentTheme); }
public static Color getTableHeaderFg(UITheme t) {
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
}
public static Color getTableRowEven() { return getTableRowEven(currentTheme); }
public static Color getTableRowEven(UITheme t) {
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
}
public static Color getTableRowOdd() { return getTableRowOdd(currentTheme); }
public static Color getTableRowOdd(UITheme t) {
return t == UITheme.DARK ? new Color(33, 33, 33) : new Color(246, 247, 249);
}
public static Color getTableGrid() { return getTableGrid(currentTheme); }
public static Color getTableGrid(UITheme t) {
return t == UITheme.DARK ? new Color(55, 55, 55) : new Color(225, 225, 225);
}
public static Color getCodeAreaBg() { return getCodeAreaBg(currentTheme); }
public static Color getCodeAreaBg(UITheme t) {
return t == UITheme.DARK ? new Color(22, 22, 22) : new Color(252, 252, 252);
}
public static Color getCodeAreaFg() { return getCodeAreaFg(currentTheme); }
public static Color getCodeAreaFg(UITheme t) {
return t == UITheme.DARK ? new Color(80, 230, 80) : new Color(0, 120, 0);
}
public static Color getStatusBarBg() { return getStatusBarBg(currentTheme); }
public static Color getStatusBarBg(UITheme t) {
return t == UITheme.DARK ? new Color(20, 20, 20) : new Color(235, 235, 238);
}
public static Color getStatusBarBorder() { return getStatusBarBorder(currentTheme); }
public static Color getStatusBarBorder(UITheme t) {
return t == UITheme.DARK ? new Color(42, 42, 42) : new Color(210, 210, 215);
}
public static Color getOiaFgNormal() { return getOiaFgNormal(currentTheme); }
public static Color getOiaFgNormal(UITheme t) {
return t == UITheme.DARK ? new Color(50, 205, 50) : new Color(0, 130, 0);
}
public static Color getOiaFgDim() { return getOiaFgDim(currentTheme); }
public static Color getOiaFgDim(UITheme t) {
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(90, 90, 90);
}
public static Color getOiaFgAlert() { return getOiaFgAlert(currentTheme); }
public static Color getOiaFgAlert(UITheme t) {
return t == UITheme.DARK ? new Color(255, 80, 80) : new Color(190, 20, 20);
}
public static Color getOiaFgWarn() { return getOiaFgWarn(currentTheme); }
public static Color getOiaFgWarn(UITheme t) {
return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
}
// =========================================================================
// Button Variants & Color helpers
// =========================================================================
public enum ButtonVariant {
DEFAULT,
PRIMARY,
CANCEL,
DANGER,
ACCENT
}
public static Color getButtonBg(ButtonVariant variant) {
return getButtonBg(variant, currentTheme, false, false);
}
public static Color getButtonBg(ButtonVariant variant, UITheme t) {
return getButtonBg(variant, t, false, false);
}
public static Color getButtonBg(ButtonVariant variant, UITheme t, boolean hover, boolean pressed) {
switch (variant) {
case PRIMARY:
if (pressed) return new Color(25, 105, 35);
if (hover) return new Color(38, 145, 48);
return new Color(32, 128, 42);
case DANGER:
if (pressed) return new Color(125, 30, 30);
if (hover) return new Color(165, 45, 45);
return new Color(145, 38, 38);
case ACCENT:
if (pressed) return new Color(35, 80, 145);
if (hover) return new Color(55, 115, 195);
return new Color(45, 100, 175);
case CANCEL:
case DEFAULT:
default:
if (t == UITheme.DARK) {
if (pressed) return new Color(42, 42, 42);
if (hover) return new Color(68, 68, 68);
return new Color(56, 56, 56);
} else {
if (pressed) return new Color(210, 210, 212);
if (hover) return new Color(228, 228, 232);
return new Color(238, 238, 240);
}
}
}
public static Color getButtonFg(ButtonVariant variant, UITheme t) {
switch (variant) {
case PRIMARY:
case DANGER:
case ACCENT:
return Color.WHITE;
case CANCEL:
case DEFAULT:
default:
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
}
}
// =========================================================================
// UIManager Defaults Configuration
// =========================================================================
public static void applyUIManagerDefaults(UITheme theme) {
Color bgMain = getBgMain(theme);
Color bgPanel = getBgPanel(theme);
Color bgComp = getBgComponent(theme);
Color fgMain = getFgMain(theme);
Color fgMuted = getFgMuted(theme);
Color border = getBorder(theme);
Color selBg = getSelectionBg(theme);
Color selFg = getSelectionFg(theme);
UIManager.put("Panel.background", bgPanel);
UIManager.put("Panel.foreground", fgMain);
UIManager.put("Label.foreground", fgMain);
UIManager.put("TextField.background", bgComp);
UIManager.put("TextField.foreground", fgMain);
UIManager.put("TextField.caretForeground", fgMain);
UIManager.put("TextField.selectionBackground", selBg);
UIManager.put("TextField.selectionForeground", selFg);
UIManager.put("TextArea.background", bgComp);
UIManager.put("TextArea.foreground", fgMain);
UIManager.put("TextArea.caretForeground", fgMain);
UIManager.put("TextArea.selectionBackground", selBg);
UIManager.put("TextArea.selectionForeground", selFg);
UIManager.put("Button.background", getBgComponentLighter(theme));
UIManager.put("Button.foreground", fgMain);
UIManager.put("Button.select", selBg);
UIManager.put("ComboBox.background", bgComp);
UIManager.put("ComboBox.foreground", fgMain);
UIManager.put("ComboBox.selectionBackground", selBg);
UIManager.put("ComboBox.selectionForeground", selFg);
UIManager.put("Table.background", bgComp);
UIManager.put("Table.foreground", fgMain);
UIManager.put("Table.selectionBackground", selBg);
UIManager.put("Table.selectionForeground", selFg);
UIManager.put("Table.gridColor", getTableGrid(theme));
UIManager.put("TableHeader.background", getTableHeaderBg(theme));
UIManager.put("TableHeader.foreground", getTableHeaderFg(theme));
UIManager.put("ScrollPane.background", bgPanel);
UIManager.put("Viewport.background", bgComp);
UIManager.put("MenuBar.background", getMenuBarBg(theme));
UIManager.put("MenuBar.foreground", getMenuBarFg(theme));
UIManager.put("Menu.background", getMenuBarBg(theme));
UIManager.put("Menu.foreground", getMenuBarFg(theme));
UIManager.put("Menu.selectionBackground", selBg);
UIManager.put("Menu.selectionForeground", selFg);
UIManager.put("PopupMenu.background", getMenuPopupBg(theme));
UIManager.put("PopupMenu.foreground", getMenuItemFg(theme));
UIManager.put("MenuItem.background", getMenuPopupBg(theme));
UIManager.put("MenuItem.foreground", getMenuItemFg(theme));
UIManager.put("MenuItem.selectionBackground", selBg);
UIManager.put("MenuItem.selectionForeground", selFg);
UIManager.put("CheckBoxMenuItem.background", getMenuPopupBg(theme));
UIManager.put("CheckBoxMenuItem.foreground", getMenuItemFg(theme));
UIManager.put("CheckBoxMenuItem.selectionBackground", selBg);
UIManager.put("CheckBoxMenuItem.selectionForeground", selFg);
UIManager.put("RadioButtonMenuItem.background", getMenuPopupBg(theme));
UIManager.put("RadioButtonMenuItem.foreground", getMenuItemFg(theme));
UIManager.put("RadioButtonMenuItem.selectionBackground", selBg);
UIManager.put("RadioButtonMenuItem.selectionForeground", selFg);
UIManager.put("Separator.background", border);
UIManager.put("Separator.foreground", border);
UIManager.put("TabbedPane.background", getTabBg(theme));
UIManager.put("TabbedPane.foreground", getTabFg(theme));
UIManager.put("TabbedPane.selected", getTabSelectedBg(theme));
UIManager.put("TabbedPane.selectHighlight", getTabSelectedBg(theme));
UIManager.put("CheckBox.background", bgPanel);
UIManager.put("CheckBox.foreground", fgMain);
UIManager.put("RadioButton.background", bgPanel);
UIManager.put("RadioButton.foreground", fgMain);
UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain);
}
// =========================================================================
// Component Styling & Custom UI Delegates
// =========================================================================
/**
* Styles a button with custom rendering, ensuring consistent high contrast
* and proper background color across Windows, macOS, and Linux.
*/
public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null;
button.setUI(new StyledButtonUI(variant));
button.setFocusPainted(false);
button.setOpaque(false);
button.setContentAreaFilled(false);
button.setBorder(BorderFactory.createEmptyBorder(6, 14, 6, 14));
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return button;
}
public static JButton createStyledButton(String text, ButtonVariant variant, Runnable action) {
JButton btn = new JButton(text);
styleButton(btn, variant);
if (action != null) {
btn.addActionListener(e -> action.run());
}
return btn;
}
public static JTextField styleTextField(JTextField field) {
if (field == null) return null;
field.setBackground(getBgComponent());
field.setForeground(getFgMain());
field.setCaretColor(getFgMain());
field.setSelectionColor(getSelectionBg());
field.setSelectedTextColor(getSelectionFg());
field.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1, true),
new EmptyBorder(4, 8, 4, 8)));
return field;
}
public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null;
area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain());
area.setSelectionColor(getSelectionBg());
area.setSelectedTextColor(getSelectionFg());
return area;
}
public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null;
box.setBackground(getBgComponent());
box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setBorder(new EmptyBorder(4, 8, 4, 8));
if (isSelected) {
setBackground(getSelectionBg());
setForeground(getSelectionFg());
} else {
setBackground(getBgComponent());
setForeground(getFgMain());
}
return this;
}
});
return box;
}
public static JTable styleTable(JTable table) {
if (table == null) return null;
table.setBackground(getBgComponent());
table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid());
table.setRowHeight(24);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBorder(new EmptyBorder(2, 6, 2, 6));
if (isSel) {
setBackground(getSelectionBg());
setForeground(getSelectionFg());
} else {
setBackground(row % 2 == 0 ? getTableRowEven() : getTableRowOdd());
setForeground(getFgMain());
}
return this;
}
});
JTableHeader header = table.getTableHeader();
if (header != null) {
header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD));
header.setDefaultRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBackground(getTableHeaderBg());
setForeground(getTableHeaderFg());
setFont(getFont().deriveFont(Font.BOLD));
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6)));
return this;
}
});
}
return table;
}
public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI());
tp.setBackground(getBgMain());
tp.setForeground(getFgMain());
return tp;
}
public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null;
bar.setUI(new StyledMenuBarUI());
bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar;
}
public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null;
menu.setUI(new StyledMenuUI());
menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg());
menu.setOpaque(false);
return menu;
}
public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null;
item.setUI(new StyledMenuItemUI());
item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg());
return item;
}
public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI());
popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1),
new EmptyBorder(4, 0, 4, 0)));
return popup;
}
public static JScrollPane styleScrollPane(JScrollPane sp) {
if (sp == null) return null;
sp.setBackground(getBgPanel());
if (sp.getViewport() != null) {
sp.getViewport().setBackground(getBgComponent());
}
sp.setBorder(new LineBorder(getBorder(), 1));
return sp;
}
public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null;
sp.setBackground(getBgComponent());
sp.setForeground(getFgMain());
sp.setBorder(new LineBorder(getBorder(), 1));
JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setBackground(getBgComponent());
tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain());
tf.setBorder(new EmptyBorder(2, 4, 2, 4));
}
return sp;
}
public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null;
cb.setOpaque(false);
cb.setForeground(getFgMain());
cb.setFocusPainted(false);
return cb;
}
public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null;
rb.setOpaque(false);
rb.setForeground(getFgMain());
rb.setFocusPainted(false);
return rb;
}
public static Border createTitledBorder(String title) {
return BorderFactory.createTitledBorder(
new LineBorder(getBorder(), 1),
title,
TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12),
getFgMain());
}
// =========================================================================
// Recursive Component Styling
// =========================================================================
public static void applyTheme(Component comp) {
applyTheme(comp, currentTheme);
}
public static void applyTheme(Component comp, UITheme theme) {
if (comp == null) return;
// Skip swatch preview panels whose background is the user-configured color
if ("colorSwatch".equals(comp.getName())) {
return;
}
if (comp instanceof JMenuBar) {
styleMenuBar((JMenuBar) comp);
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
applyTheme(((JMenuBar) comp).getMenu(i), theme);
}
return;
}
if (comp instanceof JMenu) {
styleMenu((JMenu) comp);
JMenu menu = (JMenu) comp;
for (int i = 0; i < menu.getItemCount(); i++) {
JMenuItem item = menu.getItem(i);
if (item != null) applyTheme(item, theme);
}
return;
}
if (comp instanceof JMenuItem) {
styleMenuItem((JMenuItem) comp);
return;
}
if (comp instanceof JPopupMenu) {
stylePopupMenu((JPopupMenu) comp);
for (Component child : ((JPopupMenu) comp).getComponents()) {
applyTheme(child, theme);
}
return;
}
if (comp instanceof JTabbedPane) {
styleTabbedPane((JTabbedPane) comp);
JTabbedPane tp = (JTabbedPane) comp;
for (int i = 0; i < tp.getTabCount(); i++) {
applyTheme(tp.getComponentAt(i), theme);
}
return;
}
if (comp instanceof JTable) {
styleTable((JTable) comp);
return;
}
if (comp instanceof JScrollPane) {
styleScrollPane((JScrollPane) comp);
JScrollPane sp = (JScrollPane) comp;
if (sp.getViewport() != null) {
applyTheme(sp.getViewport().getView(), theme);
}
return;
}
if (comp instanceof JButton) {
JButton btn = (JButton) comp;
if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT);
}
return;
}
if (comp instanceof JComboBox) {
styleComboBox((JComboBox<?>) comp);
return;
}
if (comp instanceof JSpinner) {
styleSpinner((JSpinner) comp);
return;
}
if (comp instanceof JTextArea) {
styleTextArea((JTextArea) comp);
return;
}
if (comp instanceof JTextField) {
styleTextField((JTextField) comp);
return;
}
if (comp instanceof JCheckBox) {
styleCheckBox((JCheckBox) comp);
return;
}
if (comp instanceof JRadioButton) {
styleRadioButton((JRadioButton) comp);
return;
}
if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme));
return;
}
if (comp instanceof JPanel) {
JPanel panel = (JPanel) comp;
if (panel.isOpaque()) {
panel.setBackground(getBgPanel(theme));
}
panel.setForeground(getFgMain(theme));
Border b = panel.getBorder();
if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme));
tb.setBorder(new LineBorder(getBorder(theme), 1));
}
} else if (comp instanceof Container) {
comp.setBackground(getBgMain(theme));
comp.setForeground(getFgMain(theme));
}
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyTheme(child, theme);
}
}
}
public static void applyThemeToWindow(Window window) {
if (window == null) return;
window.setBackground(getBgMain());
if (window instanceof RootPaneContainer) {
RootPaneContainer rpc = (RootPaneContainer) window;
if (rpc.getContentPane() != null) {
rpc.getContentPane().setBackground(getBgMain());
applyTheme(rpc.getContentPane());
}
if (rpc.getRootPane() != null && rpc.getRootPane().getJMenuBar() != null) {
applyTheme(rpc.getRootPane().getJMenuBar());
}
}
window.repaint();
}
// =========================================================================
// Custom UI Implementations
// =========================================================================
public static class StyledButtonUI extends BasicButtonUI {
private final ButtonVariant variant;
private boolean hover = false;
public StyledButtonUI(ButtonVariant variant) {
this.variant = variant != null ? variant : ButtonVariant.DEFAULT;
}
@Override
public void installUI(JComponent c) {
super.installUI(c);
AbstractButton b = (AbstractButton) c;
b.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
hover = true;
b.repaint();
}
@Override
public void mouseExited(MouseEvent e) {
hover = false;
b.repaint();
}
});
}
@Override
public void paint(Graphics g, JComponent c) {
AbstractButton b = (AbstractButton) c;
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = c.getWidth();
int h = c.getHeight();
ButtonModel model = b.getModel();
boolean pressed = model.isArmed() && model.isPressed();
boolean enabled = b.isEnabled();
UITheme t = ThemeManager.getTheme();
Color bg = enabled ? ThemeManager.getButtonBg(variant, t, hover, pressed) : (t == UITheme.DARK ? new Color(40, 40, 40) : new Color(220, 220, 220));
Color fg = enabled ? ThemeManager.getButtonFg(variant, t) : ThemeManager.getFgDisabled(t);
// Background
g2.setColor(bg);
g2.fillRoundRect(0, 0, w, h, 6, 6);
// Border
if (variant == ButtonVariant.DEFAULT || variant == ButtonVariant.CANCEL) {
g2.setColor(ThemeManager.getBorder(t));
g2.drawRoundRect(0, 0, w - 1, h - 1, 6, 6);
}
// Text / Icon
FontMetrics fm = g2.getFontMetrics(b.getFont());
String text = b.getText();
if (text != null && !text.isEmpty()) {
g2.setColor(fg);
g2.setFont(b.getFont());
int tx = (w - fm.stringWidth(text)) / 2;
int ty = (h - fm.getHeight()) / 2 + fm.getAscent();
g2.drawString(text, tx, ty);
}
g2.dispose();
}
}
public static class StyledMenuBarUI extends BasicMenuBarUI {
@Override
public void paint(Graphics g, JComponent c) {
g.setColor(ThemeManager.getMenuBarBg());
g.fillRect(0, 0, c.getWidth(), c.getHeight());
}
}
public static class StyledMenuUI extends BasicMenuUI {
@Override
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getMenuItemHoverBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
} else {
g.setColor(ThemeManager.getMenuBarBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
}
}
@Override
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getMenuItemHoverFg());
} else {
g.setColor(ThemeManager.getMenuBarFg());
}
super.paintText(g, menuItem, textRect, text);
}
}
public static class StyledMenuItemUI extends BasicMenuItemUI {
@Override
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getSelectionBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
} else {
g.setColor(ThemeManager.getMenuPopupBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
}
}
@Override
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getSelectionFg());
} else {
g.setColor(ThemeManager.getMenuItemFg());
}
super.paintText(g, menuItem, textRect, text);
}
}
public static class StyledPopupMenuUI extends BasicPopupMenuUI {
@Override
public void paint(Graphics g, JComponent c) {
g.setColor(ThemeManager.getMenuPopupBg());
g.fillRect(0, 0, c.getWidth(), c.getHeight());
}
}
public static class StyledTabbedPaneUI extends BasicTabbedPaneUI {
@Override
protected void installDefaults() {
super.installDefaults();
tabInsets = new Insets(6, 16, 6, 16);
selectedTabPadInsets = new Insets(2, 2, 2, 2);
tabAreaInsets = new Insets(4, 4, 0, 4);
}
@Override
protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(isSelected ? ThemeManager.getTabSelectedBg() : ThemeManager.getTabBg());
g2.fillRoundRect(x, y, w, h + 4, 6, 6);
g2.setColor(ThemeManager.getBorder());
g2.drawRoundRect(x, y, w - 1, h + 4, 6, 6);
g2.dispose();
}
@Override
protected void paintText(Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected) {
g.setFont(font);
g.setColor(isSelected ? ThemeManager.getTabSelectedFg() : ThemeManager.getTabFg());
g.drawString(title, textRect.x, textRect.y + metrics.getAscent());
}
@Override
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
int width = tabPane.getWidth();
int height = tabPane.getHeight();
Insets insets = tabPane.getInsets();
int x = insets.left;
int y = insets.top + calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
int w = width - insets.right - insets.left;
int h = height - insets.top - insets.bottom - calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
g.setColor(ThemeManager.getTabSelectedBg());
g.fillRect(x, y, w, h);
g.setColor(ThemeManager.getBorder());
g.drawRect(x, y, w - 1, h - 1);
}
}
}
@@ -0,0 +1,33 @@
package haus.nightmare.j3270.ui;
/**
* Supported UI themes for the j3270 Java desktop interface.
*/
public enum UITheme {
DARK("Dark Mode"),
LIGHT("Light Mode");
private final String displayName;
UITheme(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
@Override
public String toString() {
return displayName;
}
public static UITheme fromString(String name) {
if (name == null) return DARK;
String s = name.trim().toUpperCase();
if ("LIGHT".equals(s) || "LIGHT MODE".equals(s) || "LIGHT_MODE".equals(s)) {
return LIGHT;
}
return DARK;
}
}
@@ -26,25 +26,24 @@ public class UntrustedCertificateDialog extends JDialog {
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) { private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
JPanel mainPanel = new JPanel(new BorderLayout(12, 12)); JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
// Header // Header
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setBackground(new Color(30, 30, 30)); headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️"); JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
headerPanel.add(iconLabel); headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setBackground(new Color(30, 30, 30)); titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
titleLabel.setForeground(new Color(255, 180, 80)); titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel); titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
subtitleLabel.setForeground(new Color(180, 180, 180)); subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel); titleBox.add(subtitleLabel);
headerPanel.add(titleBox); headerPanel.add(titleBox);
mainPanel.add(headerPanel, BorderLayout.NORTH); mainPanel.add(headerPanel, BorderLayout.NORTH);
@@ -71,30 +70,27 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString()); JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false); detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
detailsArea.setBackground(new Color(20, 20, 20)); ThemeManager.styleTextArea(detailsArea);
detailsArea.setForeground(new Color(210, 210, 210));
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane scrollPane = new JScrollPane(detailsArea); JScrollPane scrollPane = new JScrollPane(detailsArea);
scrollPane.setPreferredSize(new Dimension(520, 260)); scrollPane.setPreferredSize(new Dimension(520, 260));
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60))); ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Buttons // Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setBackground(new Color(30, 30, 30)); buttonPanel.setOpaque(false);
JButton cancelBtn = new JButton("Cancel Connection"); JButton cancelBtn = new JButton("Cancel Connection");
cancelBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setForeground(new Color(220, 220, 220));
cancelBtn.addActionListener(e -> { cancelBtn.addActionListener(e -> {
accepted = false; accepted = false;
dispose(); dispose();
}); });
JButton trustBtn = new JButton("Connect Anyway"); JButton trustBtn = new JButton("Connect Anyway");
trustBtn.setBackground(new Color(180, 100, 40)); ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setForeground(Color.WHITE);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
trustBtn.addActionListener(e -> { trustBtn.addActionListener(e -> {
accepted = true; accepted = true;
@@ -106,6 +102,7 @@ public class UntrustedCertificateDialog extends JDialog {
mainPanel.add(buttonPanel, BorderLayout.SOUTH); mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
getRootPane().setDefaultButton(trustBtn); getRootPane().setDefaultButton(trustBtn);
} }
@@ -0,0 +1,213 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.io.File;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
public class ThemeManagerTest {
@BeforeEach
public void setup() {
ThemeManager.setTheme(UITheme.DARK);
}
@Test
public void testUIThemeEnum() {
assertEquals("Dark Mode", UITheme.DARK.getDisplayName());
assertEquals("Light Mode", UITheme.LIGHT.getDisplayName());
assertEquals(UITheme.DARK, UITheme.fromString("DARK"));
assertEquals(UITheme.DARK, UITheme.fromString("dark"));
assertEquals(UITheme.DARK, UITheme.fromString("Dark Mode"));
assertEquals(UITheme.LIGHT, UITheme.fromString("LIGHT"));
assertEquals(UITheme.LIGHT, UITheme.fromString("light"));
assertEquals(UITheme.LIGHT, UITheme.fromString("Light Mode"));
assertEquals(UITheme.DARK, UITheme.fromString("invalid_value"));
}
@Test
public void testColorTokensContrast() {
for (UITheme theme : UITheme.values()) {
Color bg = ThemeManager.getBgMain(theme);
Color fg = ThemeManager.getFgMain(theme);
assertNotNull(bg);
assertNotNull(fg);
// Compute luminance contrast
double bgLum = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue());
double fgLum = (0.299 * fg.getRed() + 0.587 * fg.getGreen() + 0.114 * fg.getBlue());
double lumDiff = Math.abs(bgLum - fgLum);
// Minimum luminance difference to guarantee readability
assertTrue(lumDiff > 120, "Theme " + theme + " text contrast luminance difference " + lumDiff + " must be > 120");
// Verify Menu Bar colors
Color menuBg = ThemeManager.getMenuBarBg(theme);
Color menuFg = ThemeManager.getMenuItemFg(theme);
double menuBgLum = (0.299 * menuBg.getRed() + 0.587 * menuBg.getGreen() + 0.114 * menuBg.getBlue());
double menuFgLum = (0.299 * menuFg.getRed() + 0.587 * menuFg.getGreen() + 0.114 * menuFg.getBlue());
assertTrue(Math.abs(menuBgLum - menuFgLum) > 100, "Menu contrast in " + theme + " must be high");
// Verify Status Bar colors
Color sbBg = ThemeManager.getStatusBarBg(theme);
Color sbNormal = ThemeManager.getOiaFgNormal(theme);
assertNotNull(sbBg);
assertNotNull(sbNormal);
// Verify Button colors
Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme);
Color btnDefFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.DEFAULT, theme);
assertNotNull(btnDefBg);
assertNotNull(btnDefFg);
Color btnPriBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.PRIMARY, theme);
Color btnPriFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.PRIMARY, theme);
assertNotNull(btnPriBg);
assertNotNull(btnPriFg);
}
}
@Test
public void testThemeChangeListeners() {
AtomicReference<UITheme> notifiedTheme = new AtomicReference<>(null);
ThemeManager.addThemeChangeListener(notifiedTheme::set);
ThemeManager.setTheme(UITheme.LIGHT);
assertEquals(UITheme.LIGHT, ThemeManager.getTheme());
assertEquals(UITheme.LIGHT, notifiedTheme.get());
assertTrue(ThemeManager.isLight());
assertFalse(ThemeManager.isDark());
ThemeManager.setTheme(UITheme.DARK);
assertEquals(UITheme.DARK, ThemeManager.getTheme());
assertEquals(UITheme.DARK, notifiedTheme.get());
assertTrue(ThemeManager.isDark());
assertFalse(ThemeManager.isLight());
}
@Test
public void testComponentStyling() {
JButton btn = new JButton("Test");
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.PRIMARY);
assertNotNull(btn.getUI());
assertTrue(btn.getUI() instanceof ThemeManager.StyledButtonUI);
JTextField tf = new JTextField("Test");
ThemeManager.styleTextField(tf);
assertNotNull(tf.getCaretColor());
JTextArea ta = new JTextArea("Test area");
ThemeManager.styleTextArea(ta);
assertNotNull(ta.getCaretColor());
JComboBox<String> cb = new JComboBox<>(new String[]{"A", "B"});
ThemeManager.styleComboBox(cb);
assertNotNull(cb.getUI());
JTable table = new JTable(new DefaultTableModel(new Object[]{"Col1"}, 1));
ThemeManager.styleTable(table);
assertNotNull(table.getSelectionBackground());
JTabbedPane tp = new JTabbedPane();
tp.addTab("Tab1", new JPanel());
ThemeManager.styleTabbedPane(tp);
assertNotNull(tp.getUI());
JMenuBar mb = new JMenuBar();
ThemeManager.styleMenuBar(mb);
JMenu menu = new JMenu("File");
ThemeManager.styleMenu(menu);
JMenuItem mi = new JMenuItem("Open");
ThemeManager.styleMenuItem(mi);
JPopupMenu popup = new JPopupMenu();
ThemeManager.stylePopupMenu(popup);
JCheckBox chk = new JCheckBox("Check");
ThemeManager.styleCheckBox(chk);
JRadioButton rb = new JRadioButton("Radio");
ThemeManager.styleRadioButton(rb);
Border tb = ThemeManager.createTitledBorder("Title");
assertNotNull(tb);
}
@Test
public void testRecursiveApplyTheme() {
JPanel root = new JPanel(new BorderLayout());
JButton b = new JButton("OK");
JTextField f = new JTextField("Data");
JTabbedPane tp = new JTabbedPane();
JPanel tabContent = new JPanel();
JLabel lbl = new JLabel("Label");
tabContent.add(lbl);
tp.addTab("T1", tabContent);
root.add(b, BorderLayout.NORTH);
root.add(f, BorderLayout.CENTER);
root.add(tp, BorderLayout.SOUTH);
ThemeManager.setTheme(UITheme.LIGHT);
ThemeManager.applyTheme(root);
assertEquals(ThemeManager.getBgPanel(UITheme.LIGHT), root.getBackground());
assertEquals(ThemeManager.getFgMain(UITheme.LIGHT), lbl.getForeground());
ThemeManager.setTheme(UITheme.DARK);
ThemeManager.applyTheme(root);
assertEquals(ThemeManager.getBgPanel(UITheme.DARK), root.getBackground());
assertEquals(ThemeManager.getFgMain(UITheme.DARK), lbl.getForeground());
}
@Test
public void testSettingsPersistence() throws Exception {
Settings.setJavaUiTheme(UITheme.LIGHT);
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
File tmpIni = File.createTempFile("j3270_test_config", ".ini");
tmpIni.deleteOnExit();
try {
// Write INI file with javaUiTheme = dark
try (PrintWriter pw = new PrintWriter(tmpIni)) {
pw.println("[j3270]");
pw.println("javaUiTheme = dark");
pw.println("fontFamily = Monospaced");
pw.println("fontSize = 16");
}
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
assertEquals(UITheme.DARK, Settings.getJavaUiTheme());
// Switch to light and export
Settings.setJavaUiTheme(UITheme.LIGHT);
File exportFile = File.createTempFile("j3270_export_config", ".ini");
exportFile.deleteOnExit();
Settings.exportToIniFile(exportFile.getAbsolutePath());
String exportedContent = new String(Files.readAllBytes(exportFile.toPath()));
assertTrue(exportedContent.toLowerCase().contains("javauitheme = light"));
// Reload exported file
Settings.loadFromIniFile(exportFile.getAbsolutePath());
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
} finally {
tmpIni.delete();
}
}
}
@@ -5,6 +5,10 @@ package haus.nightmare.lib3270j;
*/ */
public class ConnectionConfig { public class ConnectionConfig {
public enum ProxyType {
NONE, HTTP, SOCKS4, SOCKS5
}
private String host; private String host;
private int port = 23; private int port = 23;
private TerminalModel model = TerminalModel.IBM_3279_4; private TerminalModel model = TerminalModel.IBM_3279_4;
@@ -28,6 +32,21 @@ public class ConnectionConfig {
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH; private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
private String codePage = "037"; private String codePage = "037";
private String associatedPrinterLu = null; private String associatedPrinterLu = null;
private boolean nvtLocalEcho = false;
// Proxy configuration
private ProxyType proxyType = ProxyType.NONE;
private String proxyHost = null;
private int proxyPort = 0;
private String proxyUsername = null;
private String proxyPassword = null;
// STARTTLS (Telnet Option 46) dynamic socket elevation
private boolean startTlsEnabled = true;
// RFC 1572 / RFC 2877 Environment variables (Express Logon)
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
public ConnectionConfig() {} public ConnectionConfig() {}
@@ -101,6 +120,9 @@ public class ConnectionConfig {
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037"; this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
} }
public boolean isNvtLocalEcho() { return nvtLocalEcho; }
public void setNvtLocalEcho(boolean nvtLocalEcho) { this.nvtLocalEcho = nvtLocalEcho; }
public String getTerminalName() { return terminalName; } public String getTerminalName() { return terminalName; }
public void setTerminalName(String name) { this.terminalName = name; } public void setTerminalName(String name) { this.terminalName = name; }
@@ -134,9 +156,58 @@ public class ConnectionConfig {
this.dynamicCols = cols; this.dynamicCols = cols;
} }
public ProxyType getProxyType() { return proxyType; }
public void setProxyType(ProxyType proxyType) { this.proxyType = proxyType != null ? proxyType : ProxyType.NONE; }
public String getProxyHost() { return proxyHost; }
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
public int getProxyPort() { return proxyPort; }
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
public String getProxyUsername() { return proxyUsername; }
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
public String getProxyPassword() { return proxyPassword; }
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
public void setProxy(ProxyType type, String host, int port, String username, String password) {
this.proxyType = type != null ? type : ProxyType.NONE;
this.proxyHost = host;
this.proxyPort = port;
this.proxyUsername = username;
this.proxyPassword = password;
}
public boolean isStartTlsEnabled() { return startTlsEnabled; }
public void setStartTlsEnabled(boolean enabled) { this.startTlsEnabled = enabled; }
public java.util.Map<String, String> getEnvironmentVariables() { return environmentVariables; }
public void setEnvironmentVariables(java.util.Map<String, String> vars) {
this.environmentVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
}
public void setEnvironmentVariable(String name, String value) {
if (name != null) {
if (value != null) this.environmentVariables.put(name, value);
else this.environmentVariables.remove(name);
}
}
public java.util.Map<String, String> getUserVariables() { return userVariables; }
public void setUserVariables(java.util.Map<String, String> vars) {
this.userVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
}
public void setUserVariable(String name, String value) {
if (name != null) {
if (value != null) this.userVariables.put(name, value);
else this.userVariables.remove(name);
}
}
/** /**
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"), * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats. * plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
* or standard "host:port" formats.
*/ */
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) { public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
if (hostStr == null || hostStr.trim().isEmpty()) { if (hostStr == null || hostStr.trim().isEmpty()) {
@@ -146,6 +217,50 @@ public class ConnectionConfig {
boolean tls = false; boolean tls = false;
boolean tn3270e = true; boolean tn3270e = true;
// Parse --proxy=<url> or -proxy=<url> flags
ProxyType pType = ProxyType.NONE;
String pHost = null;
int pPort = 0;
String pUser = null;
String pPass = null;
String[] tokens = s.split("\\s+");
StringBuilder remaining = new StringBuilder();
for (String tok : tokens) {
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
try {
java.net.URI uri = new java.net.URI(proxyUrl);
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
if (scheme.equals("http") || scheme.equals("https")) {
pType = ProxyType.HTTP;
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
pType = ProxyType.SOCKS4;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
pType = ProxyType.SOCKS5;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
}
pHost = uri.getHost();
String userInfo = uri.getUserInfo();
if (userInfo != null) {
int colon = userInfo.indexOf(':');
if (colon >= 0) {
pUser = userInfo.substring(0, colon);
pPass = userInfo.substring(colon + 1);
} else {
pUser = userInfo;
}
}
} catch (Exception ignored) {}
} else {
if (remaining.length() > 0) remaining.append(" ");
remaining.append(tok);
}
}
s = remaining.toString().trim();
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port") // Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
boolean prefixFound = true; boolean prefixFound = true;
while (prefixFound) { while (prefixFound) {
@@ -200,6 +315,9 @@ public class ConnectionConfig {
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
config.setUseTls(tls); config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e); config.setTn3270eEnabled(tn3270e);
if (pType != ProxyType.NONE && pHost != null) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
return config; return config;
} }
@@ -54,6 +54,7 @@ public class Telnet3270Client {
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm); this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator); this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm); this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
this.inputProcessor.setOIA(oia);
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator); this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection // Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
@@ -118,6 +119,14 @@ public class Telnet3270Client {
dsProcessor.addScreenUpdateListener(l); dsProcessor.addScreenUpdateListener(l);
} }
public void addSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
fsm.addSCSInboundListener(l);
}
public void removeSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
fsm.removeSCSInboundListener(l);
}
// ========== Screen access ========== // ========== Screen access ==========
/** Get the screen buffer for rendering. */ /** Get the screen buffer for rendering. */
@@ -189,11 +198,17 @@ public class Telnet3270Client {
fsm.sendNVTString(s); fsm.sendNVTString(s);
} }
// ========== Convenience input methods ========== // ========== Convenience input methods & HoD compatibility ==========
/** Type a character at the cursor position. */ /** Type a character at the cursor position. */
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); } public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
/** Process a character at the current cursor position. */
public void processChar(char ch) { inputProcessor.processChar(ch); }
/** Process a character with explicit position and insert mode flag. */
public void processChar(char ch, int pos, boolean insert) { inputProcessor.processChar(ch, pos, insert); }
/** Type a string at the cursor position. */ /** Type a string at the cursor position. */
public void typeString(String s) { public void typeString(String s) {
for (char ch : s.toCharArray()) { for (char ch : s.toCharArray()) {
@@ -210,6 +225,7 @@ public class Telnet3270Client {
public void sendEnter() { public void sendEnter() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER); inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
} }
public void processEnter() { inputProcessor.processEnter(); }
/** Send a PF key (1-24). */ /** Send a PF key (1-24). */
public void sendPF(int number) { public void sendPF(int number) {
@@ -231,6 +247,7 @@ public class Telnet3270Client {
} }
inputProcessor.sendAid(aid); inputProcessor.sendAid(aid);
} }
public void processPF(int number) { inputProcessor.processPF(number); }
/** Send a PA key (1-3). */ /** Send a PA key (1-3). */
public void sendPA(int number) { public void sendPA(int number) {
@@ -243,44 +260,104 @@ public class Telnet3270Client {
} }
inputProcessor.sendAid(aid); inputProcessor.sendAid(aid);
} }
public void processPA(int number) { inputProcessor.processPA(number); }
/** Send Clear key. */ /** Send Clear key. */
public void sendClear() { public void sendClear() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR); inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
} }
public void processClear() { inputProcessor.processClear(); }
/** Move cursor up. */ /** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); } public void cursorUp() { inputProcessor.cursorUp(); }
public void processCursorUp() { inputProcessor.processCursorUp(); }
/** Move cursor down. */ /** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); } public void cursorDown() { inputProcessor.cursorDown(); }
public void processCursorDown() { inputProcessor.processCursorDown(); }
/** Move cursor left. */ /** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); } public void cursorLeft() { inputProcessor.cursorLeft(); }
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
/** Move cursor right. */ /** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); } public void cursorRight() { inputProcessor.cursorRight(); }
public void processCursorRight() { inputProcessor.processCursorRight(); }
/** Move cursor to home position. */ /** Move cursor to home position. */
public void cursorHome() { inputProcessor.cursorHome(); } public void cursorHome() { inputProcessor.cursorHome(); }
public void processHome() { inputProcessor.processHome(); }
/** Tab to next unprotected field. */ /** Tab to next unprotected field. */
public void tab() { inputProcessor.tab(); } public void tab() { inputProcessor.tab(); }
public void processTab() { inputProcessor.processTab(); }
/** Back-tab to previous unprotected field. */ /** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); } public void backTab() { inputProcessor.backTab(); }
public void processBackTab() { inputProcessor.processBackTab(); }
/** Move cursor to next line. */ /** Move cursor to next line. */
public void newline() { inputProcessor.newline(); } public void newline() { inputProcessor.newline(); }
public void processNewline() { inputProcessor.processNewline(); }
/** Delete character under cursor. */
public void deleteChar() { inputProcessor.deleteChar(); }
public void processDelete() { inputProcessor.processDelete(); }
/** Backspace character before cursor. */
public void backspace() { inputProcessor.backspace(); }
public void processBackspace() { inputProcessor.processBackspace(); }
/** Erase to end of field. */
public void eraseEof() { inputProcessor.eraseEof(); }
public void processEraseEOF() { inputProcessor.processEraseEOF(); }
/** Erase all unprotected fields. */ /** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); } public void eraseInput() { inputProcessor.eraseInput(); }
public void processEraseInput() { inputProcessor.processEraseInput(); }
/** Insert Duplicate order. */ /** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); } public void dup() { inputProcessor.dup(); }
public void processDup() { inputProcessor.processDup(); }
/** Insert Field Mark order. */ /** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); } public void fieldMark() { inputProcessor.fieldMark(); }
public void processFieldMark() { inputProcessor.processFieldMark(); }
/** Toggle Insert Mode. */
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
/** Move word left. */
public void processWordLeft() { inputProcessor.processWordLeft(); }
/** Move word right. */
public void processWordRight() { inputProcessor.processWordRight(); }
/** Move to field end. */
public void processFieldEnd() { inputProcessor.processFieldEnd(); }
/** Attention key. */ /** Attention key. */
public void attn() { inputProcessor.attn(); } public void attn() { inputProcessor.attn(); }
public void processAttn() { inputProcessor.processAttn(); }
/** SysReq key. */ /** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); } public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */ public void processSysReq() { inputProcessor.processSysReq(); }
public void reset() { inputProcessor.reset(); }
/** Reset (unlock keyboard, reset OIA). */
public void reset() { inputProcessor.reset(); }
public void processReset() { inputProcessor.processReset(); }
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */ /** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); } public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
/** Trigger Light Pen selection at the specified screen address. */ public boolean processCurSel() { return inputProcessor.processCurSel(); }
public boolean processCursorSelect() { return inputProcessor.processCursorSelect(); }
/** Trigger Light Pen selection at the specified screen address or cursor position. */
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); } public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
public boolean processLightPen() { return inputProcessor.processLightPen(); }
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() { public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.getProgramSymbolManager(); return dsProcessor.getProgramSymbolManager();
@@ -164,10 +164,11 @@ public class EbcdicTranslator {
} }
/** /**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode. * Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification.
*/ */
public char getAplGraphic(int ec) { public char mapAPL(int ebcdicCodePoint) {
switch (ec & 0xFF) { switch (ebcdicCodePoint & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL) // Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─' case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│' case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
@@ -198,7 +199,14 @@ public class EbcdicTranslator {
case 0xBF: return '\u00B5'; // Micro 'µ' case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬' case 0x5F: return '\u00AC'; // Not sign '¬'
default: return ebcdicToUnicode(ec & 0xFF); default: return ebcdicToUnicode(ebcdicCodePoint & 0xFF);
} }
} }
/**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
*/
public char getAplGraphic(int ec) {
return mapAPL(ec);
}
} }
@@ -144,6 +144,9 @@ public class DataStreamProcessor {
int oldCols = screen.getCols(); int oldCols = screen.getCols();
screen.erase(false); screen.erase(false);
graphicsPlane.clear(); graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, offset, length, true); processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) { if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged(); notifyScreenSizeChanged();
@@ -161,6 +164,9 @@ public class DataStreamProcessor {
int oldCols = screen.getCols(); int oldCols = screen.getCols();
screen.erase(true); screen.erase(true);
graphicsPlane.clear(); graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, offset, length, true); processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) { if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged(); notifyScreenSizeChanged();
@@ -510,11 +516,14 @@ public class DataStreamProcessor {
// Handle GE (graphic escape) prefix // Handle GE (graphic escape) prefix
byte fillCs = currentCs; byte fillCs = currentCs;
char fillUcs4 = 0;
if (pos + 4 < end && fillChar == ORDER_GE) { if (pos + 4 < end && fillChar == ORDER_GE) {
fillChar = data[pos + 4] & 0xFF; fillChar = data[pos + 4] & 0xFF;
fillCs = CS_GE; fillCs = CS_GE;
fillUcs4 = (translator != null) ? translator.mapAPL(fillChar) : (char) fillChar;
pos += 5; pos += 5;
} else { } else {
fillUcs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar) : (char) fillChar;
pos += 4; pos += 4;
} }
@@ -523,6 +532,7 @@ public class DataStreamProcessor {
ExtendedAttribute ea = screen.getCell(baddr); ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar; ea.ec = (byte) fillChar;
ea.ucs4 = fillUcs4;
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
@@ -575,6 +585,7 @@ public class DataStreamProcessor {
ea.fa = 0; // Destroy previous field attribute if any ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) geChar; ea.ec = (byte) geChar;
ea.cs = CS_GE; ea.cs = CS_GE;
ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar;
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
@@ -680,7 +691,7 @@ public class DataStreamProcessor {
// ========== Read Buffer ========== // ========== Read Buffer ==========
private void processReadBuffer() { public void processReadBuffer() {
outputPos = 0; outputPos = 0;
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
@@ -780,11 +791,21 @@ public class DataStreamProcessor {
// ========== Read Modified ========== // ========== Read Modified ==========
private void processReadModified(boolean all) { public void processReadModified(boolean all) {
if (ftDft != null && ftDft.readModified()) { if (ftDft != null && ftDft.readModified()) {
return; return;
} }
if (inputProcessor != null) {
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
inputProcessor.setLastAid(AID_NO);
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
if (outputSender != null) {
outputSender.send3270Data(data);
}
return;
}
outputPos = 0; outputPos = 0;
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
@@ -843,10 +864,11 @@ public class DataStreamProcessor {
sendOutput(); sendOutput();
} }
// ========== Write Structured Field ========== public void processWriteStructuredField(byte[] data, int offset, int length) {
if (data == null || length <= 0)
private void processWriteStructuredField(byte[] data, int offset, int length) { return;
int pos = offset + 1; // Skip WSF command byte int firstByte = data[offset] & 0xFF;
int pos = (firstByte == CMD_WSF || firstByte == SNA_CMD_WSF) ? offset + 1 : offset;
int end = offset + length; int end = offset + length;
while (pos < end) { while (pos < end) {
@@ -881,10 +903,19 @@ public class DataStreamProcessor {
case SF_ACTIVATE_PART: case SF_ACTIVATE_PART:
processActivatePartition(data, pos, fieldLen); processActivatePartition(data, pos, fieldLen);
break; break;
case SF_OUTBOUND_DS: case SF_SET_WINDOW: // 0x0F: Set Window / Modify Partition / 3270 Graphics Viewport
processSFSetWindow(data, pos, fieldLen);
break;
case SF_3270_GRAPHICS: // 0x20: 3270 Graphics / Object Control
processSFObjectControl(data, pos, fieldLen);
break;
case SF_DOCUMENT_DATA: // 0x24: Document Data / embedded SCS / Object Control
processSFDocumentData(data, pos, fieldLen);
break;
case SF_OUTBOUND_DS: // 0x40
processOutbound3270DS(data, pos, fieldLen); processOutbound3270DS(data, pos, fieldLen);
break; break;
case SF_TRANSFER_DATA: case SF_TRANSFER_DATA: // 0xD0
if (ftDft != null) { if (ftDft != null) {
ftDft.processStructuredField(data, pos, fieldLen); ftDft.processStructuredField(data, pos, fieldLen);
} else { } else {
@@ -898,95 +929,7 @@ public class DataStreamProcessor {
programSymbolManager.loadps(psData); programSymbolManager.loadps(psData);
} }
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
if (fieldLen >= 4) {
int sfSubId = data[pos + 3] & 0xFF;
switch (sfSubId) {
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
if (fieldLen > 4) {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, pos + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
StringBuilder hexDump = new StringBuilder();
for (int i = 0; i < Math.min(32, fieldLen); i++) {
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
}
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
currentGocaSubtype = sfSubId;
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x00) { // SPAN_MIDDLE
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x40) { // SPAN_LAST
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
}
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
gocaDecoder.decodeStream(data, orderOffset, orderLen);
}
notifyScreenUpdated();
}
break;
}
default:
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
break;
}
}
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
notifyScreenUpdated();
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) { if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
@@ -1274,11 +1217,275 @@ public class DataStreamProcessor {
return pid >= 0 && pid <= 255; return pid >= 0 && pid <= 255;
} }
// ========== Missing Structured Field Handlers (Phase 2) ==========
public void processSFSetWindow(byte[] data, int offset, int fieldLen) {
if (fieldLen >= 4) {
int sfSubId = data[offset + 3] & 0xFF;
switch (sfSubId) {
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
if (fieldLen > 4) {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, offset + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[offset + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
currentGocaSubtype = sfSubId;
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x00) { // SPAN_MIDDLE
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x40) { // SPAN_LAST
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
}
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
gocaDecoder.decodeStream(data, orderOffset, orderLen);
}
notifyScreenUpdated();
}
break;
}
default:
if (fieldLen >= 11) {
int xMin = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
int yMin = ((data[offset + 5] & 0xFF) << 8) | (data[offset + 6] & 0xFF);
int xMax = ((data[offset + 7] & 0xFF) << 8) | (data[offset + 8] & 0xFF);
int yMax = ((data[offset + 9] & 0xFF) << 8) | (data[offset + 10] & 0xFF);
graphicsPlane.setViewingWindow(xMin, yMin, xMax, yMax);
}
log.fine("SF 0x0F Set Window processed (len=" + fieldLen + ")");
break;
}
}
}
public void processSFSetWindow(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFSetWindow(sf, off, sf.length - off);
}
}
public void processSFObjectControl(byte[] data, int offset, int fieldLen) {
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, offset + 3, fieldLen - 3);
notifyScreenUpdated();
}
}
public void processSFObjectControl(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFObjectControl(sf, off, sf.length - off);
}
}
public void processSFDocumentData(byte[] data, int offset, int fieldLen) {
if (fieldLen > 3) {
if (embeddedScsProcessor != null) {
embeddedScsProcessor.processHostData(data, offset + 3, fieldLen - 3);
}
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, offset + 3, fieldLen - 3);
notifyScreenUpdated();
}
}
public void processSFDocumentData(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFDocumentData(sf, off, sf.length - off);
}
}
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) { public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
if (fieldLen > 5) { if (fieldLen >= 4) {
int pid = data[offset + 3] & 0xFF; int pid = data[offset + 3] & 0xFF;
screen.setActivePartition(pid); screen.setActivePartition(pid);
processRecord(data, offset + 4, fieldLen - 4, false); if (fieldLen > 4) {
processRecord(data, offset + 4, fieldLen - 4, false);
}
}
}
public void processOutbound3270DS(byte[] sf) {
if (sf != null && sf.length >= 4) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processOutbound3270DS(sf, off, sf.length - off);
}
}
// ========== Convenience Overloads ==========
public void processRecord(byte[] data) {
if (data != null && data.length > 0) {
processRecord(data, 0, data.length, false);
}
}
public void processWrite(byte[] data) {
if (data != null && data.length > 0) {
processRecord(data, 0, data.length, false);
}
}
public void processEraseWrite(byte[] data) {
if (data != null && data.length > 0) {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(false);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, 0, data.length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
}
public void processEraseWriteAlternate(byte[] data) {
if (data != null && data.length > 0) {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(true);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, 0, data.length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
}
public void processEraseAllUnprotected() {
synchronized (screen.getRenderLock()) {
screen.eraseAllUnprotected();
}
}
public void processReadModified() {
processReadModified(false);
}
public void processReadModifiedAll() {
processReadModified(true);
}
public void processWriteStructuredField(byte[] data) {
if (data != null && data.length > 0) {
processWriteStructuredField(data, 0, data.length);
}
}
public void processSFReadPartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFReadPartition(data, off, data.length - off);
}
}
public void processSFReadPartitionQuery(byte[] data) {
sendAllQueryReplies();
}
public void processSFReadPartitionQueryList(byte[] data) {
if (data != null && data.length >= 6) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
int qlStart = off + 6;
if (data.length > qlStart) {
byte[] codes = new byte[data.length - qlStart];
System.arraycopy(data, qlStart, codes, 0, codes.length);
sendRequestedQueryReplies(codes);
} else {
sendAllQueryReplies();
}
} else {
sendAllQueryReplies();
}
}
public void processSetReplyMode(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSetReplyMode(data, off, data.length - off);
}
}
public void processCreatePartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processCreatePartition(data, off, data.length - off);
}
}
public void processDestroyPartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processDestroyPartition(data, off, data.length - off);
}
}
public void processActivatePartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processActivatePartition(data, off, data.length - off);
}
}
public void processEraseReset(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processEraseReset(data, off, data.length - off);
} }
} }
@@ -294,7 +294,7 @@ public class QueryReplyBuilder {
out.write(data, 0, data.length); out.write(data, 0, data.length);
} }
private byte[] buildSummary() { public byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE; int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
@@ -307,7 +307,13 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) { public byte[] buildUsableArea() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildUsableArea(maxCols, maxRows, maxCols * maxRows);
}
public byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(19); ByteArrayOutputStream out = new ByteArrayOutputStream(19);
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java) out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
out.write(0x00); // no special character features out.write(0x00); // no special character features
@@ -316,12 +322,12 @@ public class QueryReplyBuilder {
out.write((maxRows >> 8) & 0xFF); // usable height high out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low out.write(maxRows & 0xFF); // usable height low
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING) out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING // Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
out.write((Xr_HOD >> 24) & 0xFF); out.write((Xr_HOD >> 24) & 0xFF);
out.write((Xr_HOD >> 16) & 0xFF); out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 0xFF); out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_HOD & 0xFF); out.write(Xr_HOD & 0xFF);
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING // Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
out.write((Yr_HOD >> 24) & 0xFF); out.write((Yr_HOD >> 24) & 0xFF);
out.write((Yr_HOD >> 16) & 0xFF); out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF);
@@ -359,8 +365,14 @@ public class QueryReplyBuilder {
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
private byte[] buildAlphaPartitions(int maxRows) { public byte[] buildAlphaPartitions() {
int bufSize = screen.getMaxCols() * screen.getMaxRows(); int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildAlphaPartitions(maxRows);
}
public byte[] buildAlphaPartitions(int maxRows) {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int bufSize = maxCols * maxRows;
ByteArrayOutputStream out = new ByteArrayOutputStream(4); ByteArrayOutputStream out = new ByteArrayOutputStream(4);
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition) out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high out.write((bufSize >> 8) & 0xFF); // total partition storage high
@@ -369,7 +381,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildCharsets() { public byte[] buildCharsets() {
int charW = getCharWidth(); int charW = getCharWidth();
int charH = getCharHeight(); int charH = getCharHeight();
@@ -429,7 +441,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildColor() { public byte[] buildColor() {
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total) // Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
return new byte[] { return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4, 0x00, 0x08, 0x00, (byte) 0xF4,
@@ -443,7 +455,7 @@ public class QueryReplyBuilder {
}; };
} }
private byte[] buildHighlighting() { public byte[] buildHighlighting() {
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total) // Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
return new byte[] { return new byte[] {
0x04, 0x00, (byte) 0xF0, 0x04, 0x00, (byte) 0xF0,
@@ -453,11 +465,15 @@ public class QueryReplyBuilder {
}; };
} }
private byte[] buildReplyModes() { public byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
} }
private byte[] buildDdm(int bufferSize) { public byte[] buildDdm() {
return buildDdm(4096);
}
public byte[] buildDdm(int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(8); ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved out.write(0x00); // reserved
out.write(0x00); // reserved out.write(0x00); // reserved
@@ -470,7 +486,13 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildImplicitPartition(int maxCols, int maxRows) { public byte[] buildImplicitPartition() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildImplicitPartition(maxCols, maxRows);
}
public byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22); ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters // Implicit partition sizes, 2 self-defining parameters
@@ -514,6 +536,12 @@ public class QueryReplyBuilder {
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }; return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
} }
public byte[] buildSegment() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildSegment(maxCols, maxRows);
}
public byte[] buildSegment(int maxCols, int maxRows) { public byte[] buildSegment(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000") // HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
return new byte[]{ return new byte[]{
@@ -524,10 +552,20 @@ public class QueryReplyBuilder {
}; };
} }
public byte[] buildGraphics() {
return buildSegment();
}
public byte[] buildGraphics(int maxCols, int maxRows) { public byte[] buildGraphics(int maxCols, int maxRows) {
return buildSegment(maxCols, maxRows); return buildSegment(maxCols, maxRows);
} }
public byte[] buildProcedure() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildProcedure(maxCols, maxRows);
}
public byte[] buildProcedure(int maxCols, int maxRows) { public byte[] buildProcedure(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0") // HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
return new byte[]{ return new byte[]{
@@ -540,6 +578,10 @@ public class QueryReplyBuilder {
}; };
} }
public byte[] buildGImage() {
return buildProcedure();
}
public byte[] buildGImage(int maxCols, int maxRows) { public byte[] buildGImage(int maxCols, int maxRows) {
return buildProcedure(maxCols, maxRows); return buildProcedure(maxCols, maxRows);
} }
@@ -581,6 +623,16 @@ public class QueryReplyBuilder {
appendPort(out); appendPort(out);
} }
public byte[] buildPort() {
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
appendPort(out);
return out.toByteArray();
}
public byte[] buildOemFormat() {
return buildPort();
}
public byte[] buildGrColor() { public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total) // HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
ByteArrayOutputStream out = new ByteArrayOutputStream(110); ByteArrayOutputStream out = new ByteArrayOutputStream(110);
@@ -602,6 +654,10 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
public byte[] buildGraphicColor() {
return buildGrColor();
}
public byte[] buildGColor() { public byte[] buildGColor() {
return buildGrColor(); return buildGrColor();
} }
@@ -49,4 +49,23 @@ public interface ECLConstants {
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow) int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check) int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due) int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
// Alphanumeric entry types (ECLOIA.getAlphanumericType())
int TYPE_ALPHANUMERIC = 0;
int TYPE_NUMERIC = 1;
int TYPE_DBCS = 2;
int ALPHANUMERIC_NORMAL = 0;
int ALPHANUMERIC_NUMERIC = 1;
int ALPHANUMERIC_DBCS = 2;
// Status condition flags
int STATUS_READY = 0;
int STATUS_X_SYSTEM = 1;
int STATUS_X_NUM = 2;
int STATUS_X_PROT = 3;
int STATUS_X_WAIT = 4;
int STATUS_X_INSERT = 5;
int STATUS_X_COMM = 6;
int STATUS_X_OVERFLOW = 7;
int STATUS_X_OP = 8;
} }
@@ -1,5 +1,7 @@
package haus.nightmare.lib3270j.ecl; package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -56,24 +58,34 @@ public class ECLField {
return cols > 0 ? endPos % cols : 0; return cols > 0 ? endPos % cols : 0;
} }
private byte getLiveAttribute() {
if (ps != null && ps.getScreenBuffer() != null) {
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
if (cell.isFieldAttribute()) {
return cell.fa;
}
}
return attribute;
}
public boolean isModified() { public boolean isModified() {
return faIsModified(attribute & 0xFF); return faIsModified(getLiveAttribute() & 0xFF);
} }
public boolean isProtected() { public boolean isProtected() {
return faIsProtected(attribute & 0xFF); return faIsProtected(getLiveAttribute() & 0xFF);
} }
public boolean isNumeric() { public boolean isNumeric() {
return faIsNumeric(attribute & 0xFF); return faIsNumeric(getLiveAttribute() & 0xFF);
} }
public boolean isHighIntensity() { public boolean isHighIntensity() {
return faIsHigh(attribute & 0xFF); return faIsHigh(getLiveAttribute() & 0xFF);
} }
public boolean isHidden() { public boolean isHidden() {
return faIsZero(attribute & 0xFF); return faIsZero(getLiveAttribute() & 0xFF);
} }
public boolean isDisplay() { public boolean isDisplay() {
@@ -81,11 +93,11 @@ public class ECLField {
} }
public boolean isPenSelectable() { public boolean isPenSelectable() {
return faIsSelectable(attribute & 0xFF); return faIsSelectable(getLiveAttribute() & 0xFF);
} }
public short getAttribute() { public short getAttribute() {
return (short) (attribute & 0xFF); return (short) (getLiveAttribute() & 0xFF);
} }
/** /**
@@ -140,6 +152,80 @@ public class ECLField {
} }
} }
/** Return true if this field wraps from bottom of screen to top. */
public boolean isWrapped() {
return startPos > endPos;
}
/** Last data buffer address (same as getEnd). */
public int getDataEnd() {
return endPos;
}
/**
* Check if the specified buffer address is contained within this field (including its FA).
*/
public boolean contains(int pos) {
if (ps == null) return false;
int size = ps.getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (startPos <= endPos) {
return pos >= startPos && pos <= endPos;
} else {
// Wrapped field across screen boundary
return pos >= startPos || pos <= endPos;
}
}
/**
* Check if the specified row and column is contained within this field.
*/
public boolean contains(int row, int col) {
if (ps == null) return false;
int cols = ps.getCols();
return contains(row * cols + col);
}
/**
* Set the Modified Data Tag (MDT) for this field.
*/
public void setModified(boolean modified) {
if (ps != null && ps.getScreenBuffer() != null) {
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
if (cell.isFieldAttribute()) {
if (modified) {
cell.fa = (byte) (cell.fa | FA_MODIFY);
} else {
cell.fa = (byte) (cell.fa & ~FA_MODIFY);
}
ps.getScreenBuffer().markAllChanged();
ps.getScreenBuffer().updateDisplaySnapshot();
}
}
}
/**
* Erase all character data within this field to nulls.
*/
public void erase() {
if (isProtected() || length <= 0 || ps == null || ps.getScreenBuffer() == null) return;
ScreenBuffer sb = ps.getScreenBuffer();
int size = sb.getRows() * sb.getCols();
if (size <= 0) return;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
ea.ec = 0;
ea.ucs4 = 0;
}
setModified(false);
sb.markAllChanged();
sb.updateDisplaySnapshot();
}
@Override @Override
public String toString() { public String toString() {
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]", return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
@@ -123,6 +123,52 @@ public class ECLFieldList {
return findField(row * cols + col); return findField(row * cols + col);
} }
/**
* Get the field preceding the given field in the field list.
*/
public synchronized ECLField getPreviousField(ECLField next) {
if (next == null || fields.isEmpty()) return null;
int idx = fields.indexOf(next);
if (idx > 0) {
return fields.get(idx - 1);
} else if (idx == 0) {
return fields.get(fields.size() - 1);
}
return null;
}
/**
* Find the field at the given buffer position (alias for findField).
*/
public ECLField findFieldAt(int pos) {
return findField(pos);
}
/**
* Find the field at the given row and column.
*/
public ECLField findFieldAt(int row, int col) {
return findField(row, col);
}
/**
* Find the field preceding the one at the given buffer position.
*/
public synchronized ECLField findPrevField(int pos) {
ECLField curr = findField(pos);
if (curr == null) return null;
return getPreviousField(curr);
}
/**
* Find the field succeeding the one at the given buffer position.
*/
public synchronized ECLField findNextField(int pos) {
ECLField curr = findField(pos);
if (curr == null) return null;
return getNextField(curr);
}
/** /**
* Find field containing the given text string. * Find field containing the given text string.
*/ */
@@ -147,6 +193,13 @@ public class ECLFieldList {
return f; return f;
} }
} }
// Wrap around search to beginning of field list
for (int i = 0; i < startIdx; i++) {
ECLField f = fields.get(i);
if (f.getText().contains(text)) {
return f;
}
}
return null; return null;
} }
} }
@@ -5,6 +5,7 @@ import java.util.List;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.telnet.TelnetFSM; import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
@@ -73,6 +74,92 @@ public class ECLOIA implements ECLConstants {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected(); return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
} }
private int inhibitOverride = INHIBIT_NOT_INHIBITED;
public void setInputInhibited(int reason) {
if (this.inhibitOverride != reason) {
this.inhibitOverride = reason;
notifyOIAChanged();
}
}
/**
* Get the alphanumeric character entry type allowed at current cursor position.
* Returns TYPE_ALPHANUMERIC (0), TYPE_NUMERIC (1), or TYPE_DBCS (2).
*/
public int getAlphanumericType() {
if (screen == null || !screen.isFormatted()) {
return TYPE_ALPHANUMERIC;
}
int cur = screen.getCursorAddress();
ExtendedAttribute ea = screen.getCell(cur);
if (ea != null && (ea.cs == ExtendedAttribute.CS_DBCS || ea.db != 0)) {
return TYPE_DBCS;
}
byte fa = screen.getFieldAttributeAt(cur);
if (faIsNumeric(fa & 0xFF)) {
return TYPE_NUMERIC;
}
return TYPE_ALPHANUMERIC;
}
public String getAlphanumericTypeString() {
switch (getAlphanumericType()) {
case TYPE_NUMERIC: return "N";
case TYPE_DBCS: return "D";
case TYPE_ALPHANUMERIC:
default: return "A";
}
}
public boolean isXSystem() {
return getInputInhibited() == INHIBIT_SYSTEM_LOCK;
}
public boolean isXProt() {
return getInputInhibited() == INHIBIT_PROTECTED_FIELD;
}
public boolean isXNum() {
return getInputInhibited() == INHIBIT_NUMERIC_ONLY;
}
public boolean isXWait() {
return isXSystem();
}
public boolean isXInsert() {
return isInsertMode();
}
public boolean isXComm() {
return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK;
}
public boolean isXOverflow() {
return getInputInhibited() == INHIBIT_OVERFLOW;
}
public boolean isXOperatorDue() {
return getInputInhibited() == INHIBIT_OPERATOR_DUE;
}
public String getStatusString() {
if (isCommError()) return "X-COMM";
int inhibit = getInputInhibited();
switch (inhibit) {
case INHIBIT_SYSTEM_LOCK: return "X-SYSTEM";
case INHIBIT_NUMERIC_ONLY: return "X-NUM";
case INHIBIT_PROTECTED_FIELD: return "X-PROT";
case INHIBIT_OVERFLOW: return "X-OVERFLOW";
case INHIBIT_COMM_CHECK: return "X-COMM";
case INHIBIT_OPERATOR_DUE: return "X-OP";
default:
if (isInsertMode()) return "X-INSERT";
return "READY";
}
}
/** /**
* Get the current Input Inhibited code. * Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants. * Returns one of INHIBIT_* constants from ECLConstants.
@@ -81,6 +168,9 @@ public class ECLOIA implements ECLConstants {
if (isCommError()) { if (isCommError()) {
return INHIBIT_COMM_CHECK; return INHIBIT_COMM_CHECK;
} }
if (inhibitOverride != INHIBIT_NOT_INHIBITED) {
return inhibitOverride;
}
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) { if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
return INHIBIT_SYSTEM_LOCK; return INHIBIT_SYSTEM_LOCK;
} }
@@ -33,6 +33,19 @@ public class ECLPS implements ECLConstants {
return fieldList; return fieldList;
} }
private boolean nvtMode = false;
public boolean isNVTmode() {
if (inputProcessor != null && inputProcessor.isNvtMode()) {
return true;
}
return nvtMode;
}
public void setNVTmode(boolean nvt) {
this.nvtMode = nvt;
}
public int getSize() { return screen.getRows() * screen.getCols(); } public int getSize() { return screen.getRows() * screen.getCols(); }
public int getRows() { return screen.getRows(); } public int getRows() { return screen.getRows(); }
public int getCols() { return screen.getCols(); } public int getCols() { return screen.getCols(); }
@@ -139,8 +152,8 @@ public class ECLPS implements ECLConstants {
} }
/** /**
* Search for a string in the presentation space. * Search for a string in the presentation space (0-based indexing).
* Returns 1-based or 0-based position, or -1 if not found. * Returns 0-based position, or -1 if not found.
*/ */
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) { public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
if (target == null || target.isEmpty() || screen == null) return -1; if (target == null || target.isEmpty() || screen == null) return -1;
@@ -161,7 +174,6 @@ public class ECLPS implements ECLConstants {
int targetLen = target.length(); int targetLen = target.length();
if (dir == SEARCH_FORWARD) { if (dir == SEARCH_FORWARD) {
// Forward search with wrapping
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
int pos = (startPos + i) % size; int pos = (startPos + i) % size;
if (matchesAt(screenText, target, pos, size)) { if (matchesAt(screenText, target, pos, size)) {
@@ -169,7 +181,6 @@ public class ECLPS implements ECLConstants {
} }
} }
} else { } else {
// Backward search with wrapping
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
int pos = (startPos - i + size) % size; int pos = (startPos - i + size) % size;
if (matchesAt(screenText, target, pos, size)) { if (matchesAt(screenText, target, pos, size)) {
@@ -180,6 +191,78 @@ public class ECLPS implements ECLConstants {
return -1; return -1;
} }
public int searchString(String target) {
return searchString(target, 0, 0, SEARCH_FORWARD, false);
}
public int searchString(String target, int startRow, int startCol) {
return searchString(target, startRow, startCol, SEARCH_FORWARD, false);
}
// ========== IBM HoD SearchPS / SearchPSExt (1-based API) ==========
public int SearchPS(String text) {
return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true);
}
public int SearchPS(String text, int startRow, int startCol) {
int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), SEARCH_FORWARD, false, true);
}
public int SearchPS(String text, int startRow, int startCol, int dir) {
int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), dir, false, true);
}
public int SearchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) {
int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), dir, ignoreCase, true);
}
/**
* SearchPS extended method conforming to IBM ECL specification.
* Uses 1-based positions and returns 1-based index (or 0 if not found).
*/
public synchronized int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
if (text == null || text.isEmpty() || screen == null) return 0;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return 0;
int s0 = Math.max(0, Math.min(startPos - 1, size - 1));
int e0 = Math.max(0, Math.min(endPos - 1, size - 1));
char[] fullScreen = new char[size];
getPlane(PLANE_TEXT, fullScreen, 0, size);
String screenText = new String(fullScreen);
if (ignoreCase) {
screenText = screenText.toLowerCase();
text = text.toLowerCase();
}
int count = wrap ? size : (dir == SEARCH_FORWARD ? (e0 >= s0 ? e0 - s0 + 1 : size - s0 + e0 + 1)
: (s0 >= e0 ? s0 - e0 + 1 : s0 + size - e0 + 1));
if (dir == SEARCH_FORWARD) {
for (int i = 0; i < count; i++) {
int pos = (s0 + i) % size;
if (!wrap && e0 >= s0 && pos > e0) break;
if (matchesAt(screenText, text, pos, size)) {
return pos + 1; // 1-based
}
}
} else {
for (int i = 0; i < count; i++) {
int pos = (s0 - i + size) % size;
if (!wrap && s0 >= e0 && pos < e0) break;
if (matchesAt(screenText, text, pos, size)) {
return pos + 1; // 1-based
}
}
}
return 0;
}
private boolean matchesAt(String screenText, String target, int pos, int size) { private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length(); int len = target.length();
for (int j = 0; j < len; j++) { for (int j = 0; j < len; j++) {
@@ -191,6 +274,62 @@ public class ECLPS implements ECLConstants {
return true; return true;
} }
// ========== Rectangular Block Copy & Paste ==========
/**
* Copy a rectangular text region from (sRow, sCol) to (eRow, eCol) inclusive.
*/
public synchronized String copyString(int sRow, int sCol, int eRow, int eCol) {
if (screen == null) return "";
int rows = screen.getRows();
int cols = screen.getCols();
if (rows <= 0 || cols <= 0) return "";
int minR = Math.max(0, Math.min(sRow, eRow));
int maxR = Math.min(rows - 1, Math.max(sRow, eRow));
int minC = Math.max(0, Math.min(sCol, eCol));
int maxC = Math.min(cols - 1, Math.max(sCol, eCol));
int sliceWidth = maxC - minC + 1;
StringBuilder sb = new StringBuilder();
for (int r = minR; r <= maxR; r++) {
char[] rowBuf = new char[sliceWidth];
getPlane(PLANE_TEXT, rowBuf, r * cols + minC, sliceWidth);
sb.append(rowBuf);
if (r < maxR) {
sb.append("\n");
}
}
return sb.toString();
}
/**
* Paste a multi-line rectangular block of text starting at (row, col).
*/
public synchronized int pasteString(String text, int row, int col) {
if (text == null || text.isEmpty() || screen == null) return 0;
int rows = screen.getRows();
int cols = screen.getCols();
if (rows <= 0 || cols <= 0) return 0;
String[] lines = text.split("\r?\n");
int count = 0;
for (int i = 0; i < lines.length; i++) {
int targetRow = (row + i) % rows;
String line = lines[i];
for (int c = 0; c < line.length() && (col + c) < cols; c++) {
int pos = targetRow * cols + (col + c);
setCursorPos(pos);
if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(c));
}
count++;
}
}
return count;
}
/** /**
* Paste text with line wrapping across unprotected fields. * Paste text with line wrapping across unprotected fields.
*/ */
@@ -211,15 +350,15 @@ public class ECLPS implements ECLConstants {
int curPos = screen.getCursorAddress(); int curPos = screen.getCursorAddress();
int curCol = curPos % cols; int curCol = curPos % cols;
if (endCol > 0 && curCol >= endCol) { if (endCol > 0 && curCol >= endCol) {
// Advance to next row
int nextRow = (curPos / cols + 1) % rows; int nextRow = (curPos / cols + 1) % rows;
setCursorPos(nextRow * cols); setCursorPos(nextRow * cols);
} }
inputProcessor.typeCharacter(line.charAt(i)); if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(i));
}
charsPasted++; charsPasted++;
} }
if (l < lines.length - 1) { if (l < lines.length - 1 && inputProcessor != null) {
// Newline key between lines
inputProcessor.newline(); inputProcessor.newline();
} }
} }
@@ -234,4 +373,103 @@ public class ECLPS implements ECLConstants {
inputProcessor.sendKeys(keys); inputProcessor.sendKeys(keys);
} }
} }
/**
* Send keystrokes with configurable inter-character or inter-mnemonic delay.
*/
public void sendCharacters(String keys, int delayMs) {
if (keys == null || keys.isEmpty()) return;
if (delayMs <= 0) {
sendKeys(keys);
return;
}
int i = 0;
int len = keys.length();
while (i < len) {
if (keys.charAt(i) == '[') {
int close = keys.indexOf(']', i);
if (close > i) {
String mnemonic = keys.substring(i, close + 1);
sendKeys(mnemonic);
i = close + 1;
} else {
sendKeys(keys.substring(i, i + 1));
i++;
}
} else {
sendKeys(keys.substring(i, i + 1));
i++;
}
if (delayMs > 0 && i < len) {
try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// ========== Synchronization & ECL Automation Waits ==========
/**
* Block until the specified text appears anywhere on the presentation space.
*/
public boolean waitForScreen(String text, long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (searchString(text) >= 0) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return searchString(text) >= 0;
}
/**
* Block until the specified text appears at the given (row, col) coordinate.
*/
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
String onScreen = getString(row, col, text.length());
if (text.equals(onScreen)) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return text.equals(getString(row, col, text.length()));
}
/**
* Block until the cursor moves to (row, col).
*/
public boolean waitForCursor(int row, int col, long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getCursorRow() == row && getCursorCol() == col) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getCursorRow() == row && getCursorCol() == col;
}
} }
@@ -19,21 +19,36 @@ public class FillArea {
public static class Edge { public static class Edge {
public final double x1, y1; public final double x1, y1;
public final double x2, y2; public final double x2, y2;
public final int direction; // +1 if y1 < y2 (upward), -1 if y1 > y2 (downward)
public Edge(double x1, double y1, double x2, double y2) { public Edge(double x1, double y1, double x2, double y2) {
this.x1 = x1; this.x1 = x1;
this.y1 = y1; this.y1 = y1;
this.x2 = x2; this.x2 = x2;
this.y2 = y2; this.y2 = y2;
this.direction = (y2 > y1) ? 1 : -1;
} }
} }
private int fillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private final List<Edge> edges = new ArrayList<>(); private final List<Edge> edges = new ArrayList<>();
private final List<double[]> subpathsX = new ArrayList<>(); private final List<double[]> subpathsX = new ArrayList<>();
private final List<double[]> subpathsY = new ArrayList<>(); private final List<double[]> subpathsY = new ArrayList<>();
public FillArea() {} public FillArea() {}
public FillArea(int fillRule) {
this.fillRule = fillRule;
}
public synchronized void setFillRule(int fillRule) {
this.fillRule = fillRule;
}
public synchronized int getFillRule() {
return fillRule;
}
/** /**
* Adds a single directed edge to the edge table. * Adds a single directed edge to the edge table.
*/ */
@@ -104,12 +119,47 @@ public class FillArea {
subpathsY.clear(); subpathsY.clear();
} }
/**
* Rasterizes and fills a direct polygon on the target GraphicsPlane.
*/
public synchronized void fill(GraphicsPlane plane, int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
clear();
addPolygon(px, py, numPoints);
fill(plane, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, null);
}
/** /**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane. * Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
*/ */
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern, public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, ProgramSymbolManager psm) { int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
fill(plane, fillColorArgb, patternSet, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, this.fillRule, psm);
}
private static class NodeIntersection implements Comparable<NodeIntersection> {
final double x;
final int dir;
NodeIntersection(double x, int dir) {
this.x = x;
this.dir = dir;
}
@Override
public int compareTo(NodeIntersection other) {
return Double.compare(this.x, other.x);
}
}
/**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, int fillRule, ProgramSymbolManager psm) {
if (plane == null) return; if (plane == null) return;
if (edges.isEmpty() && subpathsX.isEmpty()) return; if (edges.isEmpty() && subpathsX.isEmpty()) return;
@@ -140,7 +190,7 @@ public class FillArea {
int iMinY = Math.max(0, (int) Math.floor(minY)); int iMinY = Math.max(0, (int) Math.floor(minY));
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY)); int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
List<Double> nodeX = new ArrayList<>(); List<NodeIntersection> intersections = new ArrayList<>();
byte[] patRows = null; byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null; ProgramSymbolSet.SymbolSlot psSlot = null;
@@ -156,45 +206,84 @@ public class FillArea {
} }
for (int y = iMinY; y <= iMaxY; y++) { for (int y = iMinY; y <= iMaxY; y++) {
nodeX.clear(); intersections.clear();
double scanY = y + 0.5; double scanY = y + 0.5;
for (Edge e : edges) { for (Edge e : edges) {
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) { if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1); double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
nodeX.add(x); intersections.add(new NodeIntersection(x, e.direction));
} }
} }
Collections.sort(nodeX); Collections.sort(intersections);
for (int i = 0; i < nodeX.size(); i += 2) { if (fillRule == GocaConstants.FILL_RULE_WINDING) {
if (i + 1 >= nodeX.size()) break; // Non-Zero Winding Rule: evaluate winding count
int leftX = Math.max(0, (int) Math.round(nodeX.get(i))); int winding = 0;
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1))); for (int i = 0; i < intersections.size() - 1; i++) {
winding += intersections.get(i).dir;
if (winding != 0) {
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) { for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) { if (psSlot != null) {
int psW = psSlot.getWidth(); int psW = psSlot.getWidth();
int psH = psSlot.getHeight(); int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0; int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0; int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData(); byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
plane.setPixel(x, y, fill); plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg); plane.setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
}
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { }
plane.setPixel(x, y, fill); }
} else if (patRows != null) { } else {
int b = patRows[y & 7] & 0xFF; // Even-Odd / Alternate Rule
if (((b >> (7 - (x & 7))) & 1) != 0) { for (int i = 0; i < intersections.size(); i += 2) {
if (i + 1 >= intersections.size()) break;
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill); plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (patRows != null) {
plane.setPixel(x, y, bg); int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
} }
} }
} }
@@ -50,6 +50,8 @@ public final class GocaConstants {
public static final int G_GSMS = 0x1B; // Set Marker Size public static final int G_GSMS = 0x1B; // Set Marker Size
public static final int G_GSCP = 0x21; // Set Current Position public static final int G_GSCP = 0x21; // Set Current Position
public static final int G_GSAP = 0x22; // Arc Parameters public static final int G_GSAP = 0x22; // Arc Parameters
public static final int G_GSC = 0x22; // Segment Characteristics
public static final int G_GSVW_DEF = 0x23; // Set Viewing Window Definition
public static final int G_GSECOL = 0x26; // Set Extended Color public static final int G_GSECOL = 0x26; // Set Extended Color
public static final int G_GSVW = 0x27; // Set Viewing Window public static final int G_GSVW = 0x27; // Set Viewing Window
public static final int G_GSPT = 0x28; // Set Pattern Symbol public static final int G_GSPT = 0x28; // Set Pattern Symbol
@@ -157,6 +159,19 @@ public final class GocaConstants {
public static final int MIX_XOR = 4; public static final int MIX_XOR = 4;
public static final int MIX_UNDER = 5; public static final int MIX_UNDER = 5;
// Fill Rules (GBAR 0x68 flags)
public static final int FILL_RULE_EVEN_ODD = 0;
public static final int FILL_RULE_WINDING = 1;
// Image Formats & Compression (GBIMG 0xD1)
public static final int IMG_UNCOMPRESSED = 0;
public static final int IMG_RLE = 1;
public static final int IMG_MMR = 2;
public static final int BPP_1 = 1;
public static final int BPP_2 = 2;
public static final int BPP_4 = 4;
public static final int BPP_8 = 8;
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB) // Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
public static final int[] GOCA_COLORS = new int[] { public static final int[] GOCA_COLORS = new int[] {
0xFF00FF00, // 0: Default (Green) 0xFF00FF00, // 0: Default (Green)
@@ -33,6 +33,8 @@ public class GocaDecoder {
private int fillColor = GocaConstants.GOCA_COLORS[0]; private int fillColor = GocaConstants.GOCA_COLORS[0];
private int charDir = GocaConstants.CD_LR; private int charDir = GocaConstants.CD_LR;
private double charAngle = 0.0; private double charAngle = 0.0;
private double charShear = 0.0;
private double fractionalLineWidth = 1.0;
private int charWidth = 9; private int charWidth = 9;
private int charHeight = 16; private int charHeight = 16;
private int charSet = 0; private int charSet = 0;
@@ -42,11 +44,16 @@ public class GocaDecoder {
private int arcParamR = 0; private int arcParamR = 0;
private int arcParamS = 1; private int arcParamS = 1;
private boolean segChained = false;
private boolean segDynamic = false;
private boolean segVisible = true;
private ProgramSymbolManager programSymbolManager; private ProgramSymbolManager programSymbolManager;
// Area accumulation // Area accumulation
private boolean inArea = false; private boolean inArea = false;
private boolean areaDrawBoundary = true; private boolean areaDrawBoundary = true;
private int areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private boolean areaFill = true; private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>(); private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>(); private final List<Integer> areaPointsY = new ArrayList<>();
@@ -59,6 +66,8 @@ public class GocaDecoder {
private int imgY = 0; private int imgY = 0;
private int imgWidth = 0; private int imgWidth = 0;
private int imgHeight = 0; private int imgHeight = 0;
private int imgBitDepth = GocaConstants.BPP_1;
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
private final List<Byte> imgBuffer = new ArrayList<>(); private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A) // Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
@@ -198,6 +207,89 @@ public class GocaDecoder {
} }
} }
public double getCharAngle() {
return charAngle;
}
public synchronized void setCharAngle(double angle) {
this.charAngle = angle;
}
public double getCharShear() {
return charShear;
}
public synchronized void setCharShear(double shear) {
this.charShear = shear;
}
public double getFractionalLineWidth() {
return fractionalLineWidth;
}
public synchronized void setFractionalLineWidth(double flw) {
this.fractionalLineWidth = Math.max(0.1, flw);
if (plane != null) {
plane.setFractionalLineWidth(this.fractionalLineWidth);
}
}
public boolean isSegChained() {
return segChained;
}
public boolean isSegDynamic() {
return segDynamic;
}
public boolean isSegVisible() {
return segVisible;
}
/**
* Processes GOCA order 0x23 / 0x27 Viewing Window clipping viewport.
*/
public synchronized void processViewingWindow(byte[] data) {
if (data == null || data.length < 8) return;
int xMin = readCoord(data, 0);
int yMin = readCoord(data, 2);
int xMax = readCoord(data, 4);
int yMax = readCoord(data, 6);
logger.info(String.format("GOCA processViewingWindow: [%d..%d, %d..%d]", xMin, xMax, yMin, yMax));
if (plane != null) {
plane.setViewingWindow(xMin, yMin, xMax, yMax);
}
}
/**
* Processes GOCA order 0x22 Segment Characteristics (chained/non-chained, dynamic, visible).
*/
public synchronized void processSegmentCharacteristics(byte[] data) {
if (data == null || data.length < 1) return;
int flags = data[0] & 0xFF;
this.segChained = (flags & 0x80) != 0;
this.segDynamic = (flags & 0x40) != 0;
this.segVisible = (flags & 0x20) == 0;
logger.info(String.format("GOCA processSegmentCharacteristics: flags=0x%02x (chained=%b, dynamic=%b, visible=%b)",
flags, segChained, segDynamic, segVisible));
}
/**
* Processes GOCA order 0x11 Fractional Line Width calculation.
*/
public synchronized void processFractionalLineWidth(byte[] data) {
if (data == null || data.length < 1) return;
int intPart = data[0] & 0xFF;
int fracPart = (data.length > 1) ? (data[1] & 0xFF) : 0;
double flw = intPart + (fracPart / 256.0);
if (flw <= 0.0) flw = 1.0;
this.fractionalLineWidth = flw;
if (plane != null) {
plane.setFractionalLineWidth(flw);
}
logger.info("GOCA processFractionalLineWidth: flw=" + flw);
}
public synchronized void resetDefaults() { public synchronized void resetDefaults() {
curX = 0; curX = 0;
curY = 0; curY = 0;
@@ -217,6 +309,10 @@ public class GocaDecoder {
bgColor = GocaConstants.GOCA_COLORS[8]; // Black bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID; lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL; lineWidth = GocaConstants.LW_NORMAL;
fractionalLineWidth = 1.0;
if (plane != null) {
plane.setFractionalLineWidth(1.0);
}
markerType = GocaConstants.MK_PLUS; markerType = GocaConstants.MK_PLUS;
markerSize = 5; markerSize = 5;
markerColor = curColor; markerColor = curColor;
@@ -226,14 +322,18 @@ public class GocaDecoder {
fillColor = curColor; fillColor = curColor;
charDir = GocaConstants.CD_LR; charDir = GocaConstants.CD_LR;
charAngle = 0.0; charAngle = 0.0;
charShear = 0.0;
charSet = 0; charSet = 0;
charPrecision = GocaConstants.CP_STRING; charPrecision = GocaConstants.CP_STRING;
inArea = false; inArea = false;
areaDrawBoundary = true; areaDrawBoundary = true;
areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
areaFill = true; areaFill = true;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
inImage = false; inImage = false;
imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
imgBuffer.clear(); imgBuffer.clear();
} }
@@ -270,6 +370,10 @@ public class GocaDecoder {
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
} }
// Fractional Line Width (0x11): 2-byte operand [int][frac] or 1-byte operand [int]
if (order == GocaConstants.G_GSFLW) {
return (idx + 2 < end) ? 3 : 2;
}
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.) // All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
if (order < 0x20) { if (order < 0x20) {
return 2; return 2;
@@ -497,7 +601,15 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27) case GocaConstants.G_GSVW_DEF:
case GocaConstants.G_GSVW: { // Set Viewing Window (0x23 / 0x27)
if (payloadLen >= 8 && idx + 9 < end) {
byte[] vwData = new byte[8];
System.arraycopy(inputData, idx + 2, vwData, 0, 8);
processViewingWindow(vwData);
} else if (payloadLen == 0 && plane != null) {
plane.clearViewingWindow();
}
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -558,6 +670,17 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GSCR: { // Set Character Shear (0x35) case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
if (payloadLen >= 4 && idx + 5 < end) {
int sx = readCoord(inputData, idx + 2);
int sy = readCoord(inputData, idx + 4);
if (sx != 0 || sy != 0) {
charShear = Math.toDegrees(Math.atan2(sx, sy));
}
} else if (payloadLen >= 2 && idx + 3 < end) {
int intPart = inputData[idx + 2];
int fracPart = (payloadLen >= 2) ? (inputData[idx + 3] & 0xFF) : 0;
charShear = (intPart + fracPart / 256.0) * 45.0;
}
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -581,8 +704,16 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSFLW: { // Set Fractional Line Width (0x11)
if (orderLen >= 2 && idx + 1 < end) {
byte[] flwData = new byte[orderLen - 1];
System.arraycopy(inputData, idx + 1, flwData, 0, flwData.length);
processFractionalLineWidth(flwData);
}
idx += orderLen;
break;
}
case 0x06: case 0x06:
case 0x11:
case GocaConstants.G_GSLT: { // Set Line Type (0x18) case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF; lineType = inputData[idx + 1] & 0xFF;
idx += orderLen; idx += orderLen;
@@ -661,8 +792,9 @@ public class GocaDecoder {
case GocaConstants.G_GBAR: { // Begin Area (0x68) case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0; boolean drawBoundary = (flags & 0x80) != 0;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary)); int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
beginArea(drawBoundary); logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
beginArea(drawBoundary, fillRule);
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -770,7 +902,18 @@ public class GocaDecoder {
int y = readCoord(inputData, idx + 4); int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6); int w = readCoord(inputData, idx + 6);
int h = readCoord(inputData, idx + 8); int h = readCoord(inputData, idx + 8);
beginImage(x, y, w, h); int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 9) {
int fmt = inputData[idx + 10] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 10) {
compression = inputData[idx + 11] & 0xFF;
}
beginImage(x, y, w, h, bitDepth, compression);
} }
idx += orderLen; idx += orderLen;
break; break;
@@ -887,16 +1030,21 @@ public class GocaDecoder {
} }
private void beginArea(boolean drawBoundary) { private void beginArea(boolean drawBoundary) {
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
}
private void beginArea(boolean drawBoundary, int fillRule) {
this.inArea = true; this.inArea = true;
this.areaDrawBoundary = drawBoundary; this.areaDrawBoundary = drawBoundary;
this.areaFillRule = fillRule;
this.areaFill = true; this.areaFill = true;
this.fillColor = this.curColor; this.fillColor = this.curColor;
this.areaPointsX.clear(); this.areaPointsX.clear();
this.areaPointsY.clear(); this.areaPointsY.clear();
this.areaPolygons.clear(); this.areaPolygons.clear();
this.currentPolyPts = 0; this.currentPolyPts = 0;
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d", logger.info(String.format("GOCA beginArea: drawBoundary=%b fillRule=%d fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillColor, patternSet, pattern)); drawBoundary, fillRule, fillColor, patternSet, pattern));
} }
private void endArea() { private void endArea() {
@@ -928,11 +1076,11 @@ public class GocaDecoder {
polyCounts[i] = areaPolygons.get(i); polyCounts[i] = areaPolygons.get(i);
} }
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d", logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d fillRule=%d",
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix)); n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix, areaFillRule));
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet, plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor); pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor, areaFillRule);
inArea = false; inArea = false;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
@@ -971,11 +1119,17 @@ public class GocaDecoder {
} }
private void beginImage(int x, int y, int w, int h) { private void beginImage(int x, int y, int w, int h) {
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
}
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
this.inImage = true; this.inImage = true;
this.imgX = x; this.imgX = x;
this.imgY = y; this.imgY = y;
this.imgWidth = w; this.imgWidth = w;
this.imgHeight = h; this.imgHeight = h;
this.imgBitDepth = bitDepth;
this.imgCompression = compression;
this.imgBuffer.clear(); this.imgBuffer.clear();
} }
@@ -991,7 +1145,7 @@ public class GocaDecoder {
int px = plane.mapX(imgX); int px = plane.mapX(imgX);
int py = plane.mapY(imgY); int py = plane.mapY(imgY);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor); plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
inImage = false; inImage = false;
imgBuffer.clear(); imgBuffer.clear();
} }
@@ -1328,7 +1482,7 @@ public class GocaDecoder {
} }
String text = new String(chars); String text = new String(chars);
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text, plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle); curColor, cw, ch, charDir, charAngle, charShear);
} else if (charSet != 0 && programSymbolManager != null) { } else if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) { for (int i = 0; i < textLen; i++) {
int code = data[pos + i] & 0xFF; int code = data[pos + i] & 0xFF;
@@ -1366,7 +1520,7 @@ public class GocaDecoder {
} }
String text = new String(chars); String text = new String(chars);
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text, plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle); curColor, cw, ch, charDir, charAngle, charShear);
} }
switch (charDir) { switch (charDir) {
@@ -1395,11 +1549,15 @@ public class GocaDecoder {
* Draws a transformed character string (matching HODDecoder.drawGCS). * Draws a transformed character string (matching HODDecoder.drawGCS).
*/ */
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) { public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) {
drawGcs(x, y, text, color, cw, ch, dir, angle, 0.0);
}
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle, double shearAngle) {
if (plane != null && text != null && !text.isEmpty()) { if (plane != null && text != null && !text.isEmpty()) {
if (charPrecision == GocaConstants.CP_STROKE) { if (charPrecision == GocaConstants.CP_STROKE) {
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle); plane.drawVectorText(x, y, text, color, cw, ch, dir, angle, shearAngle);
} else { } else {
plane.drawText(x, y, text, color, cw, ch, dir, angle); plane.drawText(x, y, text, color, cw, ch, dir, angle, shearAngle);
} }
} }
} }
@@ -1408,12 +1566,16 @@ public class GocaDecoder {
* Draws an EBCDIC byte buffer as a transformed character string. * Draws an EBCDIC byte buffer as a transformed character string.
*/ */
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) { public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) {
drawGcs(ebcdicData, offset, length, color, cw, ch, dir, angle, 0.0);
}
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle, double shearAngle) {
if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return; if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
char[] chars = new char[length]; char[] chars = new char[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]); chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]);
} }
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle); drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle, shearAngle);
} }
/** /**
@@ -106,4 +106,11 @@ public class GraphicInputBuilder {
return sf; return sf;
} }
/**
* Builds the 56-byte Graphic Input Structured Field for pick correlation with aperture.
*/
public static byte[] buildPickCorrelation(int gocaX, int gocaY, int aperture) {
return buildGraphicInput(gocaX, gocaY, 1, true, false, false);
}
} }
@@ -148,6 +148,7 @@ public class GraphicsPlane {
this.canvasWidth = w; this.canvasWidth = w;
this.canvasHeight = h; this.canvasHeight = h;
this.rgbBuffer = newBuffer; this.rgbBuffer = newBuffer;
updateViewingWindowPixels();
} }
public synchronized void clear() { public synchronized void clear() {
@@ -170,6 +171,59 @@ public class GraphicsPlane {
return rgbBuffer; return rgbBuffer;
} }
private int viewingWindowXMin = 0;
private int viewingWindowYMin = 0;
private int viewingWindowXMax = 0;
private int viewingWindowYMax = 0;
private boolean viewingWindowActive = false;
private int clipPixelXMin = 0;
private int clipPixelYMin = 0;
private int clipPixelXMax = 0;
private int clipPixelYMax = 0;
private double fractionalLineWidth = 1.0;
public synchronized void setViewingWindow(int xMin, int yMin, int xMax, int yMax) {
this.viewingWindowXMin = xMin;
this.viewingWindowYMin = yMin;
this.viewingWindowXMax = xMax;
this.viewingWindowYMax = yMax;
this.viewingWindowActive = true;
updateViewingWindowPixels();
}
public synchronized void clearViewingWindow() {
this.viewingWindowActive = false;
}
public synchronized boolean isViewingWindowActive() {
return viewingWindowActive;
}
public synchronized int getViewingWindowXMin() { return viewingWindowXMin; }
public synchronized int getViewingWindowYMin() { return viewingWindowYMin; }
public synchronized int getViewingWindowXMax() { return viewingWindowXMax; }
public synchronized int getViewingWindowYMax() { return viewingWindowYMax; }
public synchronized void setFractionalLineWidth(double flw) {
this.fractionalLineWidth = Math.max(0.1, flw);
}
public synchronized double getFractionalLineWidth() {
return fractionalLineWidth;
}
private void updateViewingWindowPixels() {
if (!viewingWindowActive) return;
int px1 = mapX(viewingWindowXMin);
int px2 = mapX(viewingWindowXMax);
int py1 = mapY(viewingWindowYMin);
int py2 = mapY(viewingWindowYMax);
this.clipPixelXMin = Math.max(0, Math.min(px1, px2));
this.clipPixelXMax = Math.min(canvasWidth - 1, Math.max(px1, px2));
this.clipPixelYMin = Math.max(0, Math.min(py1, py2));
this.clipPixelYMax = Math.min(canvasHeight - 1, Math.max(py1, py2));
}
public int getCanvasWidth() { public int getCanvasWidth() {
return canvasWidth; return canvasWidth;
} }
@@ -313,6 +367,11 @@ public class GraphicsPlane {
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending. * Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
*/ */
public synchronized void setPixel(int x, int y, int colorArgb) { public synchronized void setPixel(int x, int y, int colorArgb) {
if (viewingWindowActive) {
if (x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax) {
return;
}
}
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
int srcA = (colorArgb >>> 24) & 0xFF; int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return; if (srcA == 0) return;
@@ -447,11 +506,15 @@ public class GraphicsPlane {
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) { private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
if (brightness <= 0.0) return; if (brightness <= 0.0) return;
if (lineWidth == GocaConstants.LW_THICK) { if (lineWidth == GocaConstants.LW_THICK || fractionalLineWidth >= 1.5) {
int extra = (int) Math.round(Math.max(1, fractionalLineWidth - 0.5));
setPixelCoverage(x, y, colorRgb, 1.0); setPixelCoverage(x, y, colorRgb, 1.0);
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness)); for (int dx = -extra; dx <= extra; dx++) {
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness)); for (int dy = -extra; dy <= extra; dy++) {
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7)); if (dx == 0 && dy == 0) continue;
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
}
}
} else { } else {
// Perceptual gamma correction for crisp contrast on dark backgrounds // Perceptual gamma correction for crisp contrast on dark backgrounds
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15); double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
@@ -641,6 +704,9 @@ public class GraphicsPlane {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb); fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
} }
/**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/
/** /**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization. * Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/ */
@@ -648,25 +714,41 @@ public class GraphicsPlane {
int fillColorArgb, int patternSet, int pattern, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) { int bgMix, int bgColorArgb) {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, patternSet, pattern,
drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, GocaConstants.FILL_RULE_EVEN_ODD);
}
private static class NodeInter implements Comparable<NodeInter> {
final int x;
final int dir;
NodeInter(int x, int dir) {
this.x = x;
this.dir = dir;
}
@Override
public int compareTo(NodeInter o) {
return Integer.compare(this.x, o.x);
}
}
/**
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, int fillRule) {
if (px == null || py == null || numPoints < 3) return; if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb; int bg = bgColorArgb;
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
// Background Mix (GSBMX / bgMix):
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) && boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0); (bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
// Find polygon vertical bounds across all points
int minY = py[0]; int minY = py[0];
int maxY = py[0]; int maxY = py[0];
for (int i = 1; i < numPoints; i++) { for (int i = 1; i < numPoints; i++) {
@@ -676,7 +758,7 @@ public class GraphicsPlane {
minY = Math.max(0, minY); minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY); maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>(); List<NodeInter> nodeIntersections = new ArrayList<>();
byte[] patRows = null; byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null; ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && programSymbolManager != null) { if (patternSet >= 0x40 && programSymbolManager != null) {
@@ -691,7 +773,7 @@ public class GraphicsPlane {
} }
for (int y = minY; y <= maxY; y++) { for (int y = minY; y <= maxY; y++) {
nodeX.clear(); nodeIntersections.clear();
int offset = 0; int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1; int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) { for (int p = 0; p < polyCount; p++) {
@@ -705,7 +787,8 @@ public class GraphicsPlane {
int xj = px[offset + j]; int xj = px[offset + j];
if ((yi < y && yj >= y) || (yj < y && yi >= y)) { if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi)); int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
nodeX.add(x); int dir = (yj > yi) ? 1 : -1;
nodeIntersections.add(new NodeInter(x, dir));
} }
j = i; j = i;
} }
@@ -713,35 +796,72 @@ public class GraphicsPlane {
offset += pLen; offset += pLen;
} }
Collections.sort(nodeX); Collections.sort(nodeIntersections);
for (int i = 0; i < nodeX.size(); i += 2) { if (fillRule == GocaConstants.FILL_RULE_WINDING) {
if (i + 1 >= nodeX.size()) break; int winding = 0;
int leftX = Math.max(0, nodeX.get(i)); for (int i = 0; i < nodeIntersections.size() - 1; i++) {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); winding += nodeIntersections.get(i).dir;
if (winding != 0) {
int leftX = Math.max(0, nodeIntersections.get(i).x);
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
for (int x = leftX; x <= rightX; x++) { for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) { if (psSlot != null) {
int psW = psSlot.getWidth(); int psW = psSlot.getWidth();
int psH = psSlot.getHeight(); int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0; int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0; int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData(); byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg); setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
}
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { }
setPixel(x, y, fill); }
} else { } else {
int b = patRows[y & 7] & 0xFF; for (int i = 0; i < nodeIntersections.size(); i += 2) {
if (((b >> (7 - (x & 7))) & 1) != 0) { if (i + 1 >= nodeIntersections.size()) break;
int leftX = Math.max(0, nodeIntersections.get(i).x);
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background) } else {
setPixel(x, y, bg); int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} }
} }
} }
@@ -857,19 +977,29 @@ public class GraphicsPlane {
*/ */
public synchronized void drawText(double x, double y, String text, int colorArgb, public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) { double cellWidth, double cellHeight, int dir, double angle) {
drawText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
}
public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
if (textRenderer != null) { if (textRenderer != null) {
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle); textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
hasContent = true; hasContent = true;
updateCount++; updateCount++;
} else { } else {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle); drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
} }
} }
public synchronized void drawText(int x, int y, String text, int colorArgb, public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) { int cellWidth, int cellHeight, int dir, double angle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle); drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
} }
/** /**
@@ -877,6 +1007,11 @@ public class GraphicsPlane {
*/ */
public synchronized void drawVectorText(double x, double y, String text, int colorArgb, public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) { double cellWidth, double cellHeight, int dir, double angle) {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
}
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
@@ -884,25 +1019,37 @@ public class GraphicsPlane {
double ch = cellHeight > 0 ? cellHeight : 20.0; double ch = cellHeight > 0 ? cellHeight : 20.0;
double curX = x; double curX = x;
double curY = y; double curY = y;
if (dir == GocaConstants.CD_TB) {
curY += ch; double radAngle = Math.toRadians(angle);
} else if (dir == GocaConstants.CD_RL) { double cosA = Math.cos(radAngle);
curX -= cw; double sinA = Math.sin(radAngle);
if (angle == 0.0) {
if (dir == GocaConstants.CD_TB) {
curY += ch;
} else if (dir == GocaConstants.CD_RL) {
curX -= cw;
}
} }
for (int i = 0; i < text.length(); i++) { for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i); char c = text.charAt(i);
drawVssChar(curX, curY, c, color, cw, ch); drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
switch (dir) { if (angle != 0.0) {
case GocaConstants.CD_TB: curY += ch; break; curX += cw * cosA;
case GocaConstants.CD_RL: curX -= cw; break; curY += cw * sinA;
case GocaConstants.CD_BT: curY -= ch; break; } else {
case GocaConstants.CD_LR: switch (dir) {
case GocaConstants.CD_DEFAULT: case GocaConstants.CD_TB: curY += ch; break;
default: case GocaConstants.CD_RL: curX -= cw; break;
curX += cw; case GocaConstants.CD_BT: curY -= ch; break;
break; case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
}
} }
} }
hasContent = true; hasContent = true;
@@ -911,10 +1058,19 @@ public class GraphicsPlane {
public synchronized void drawVectorText(int x, int y, String text, int colorArgb, public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) { int cellWidth, int cellHeight, int dir, double angle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle); drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
} }
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) { private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch, double angle, double shearAngle) {
int code = (int) c; int code = (int) c;
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) { if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
return; return;
@@ -924,6 +1080,11 @@ public class GraphicsPlane {
return; return;
} }
double radAngle = Math.toRadians(angle);
double cosA = Math.cos(radAngle);
double sinA = Math.sin(radAngle);
double tanShear = Math.tan(Math.toRadians(shearAngle));
int ptr = offset; int ptr = offset;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) { while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF; int order = VectorSymbolData.vss_data[ptr] & 0xFF;
@@ -941,8 +1102,18 @@ public class GraphicsPlane {
for (int p = 0; p < numPoints; p++) { for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF); int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF); int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
py[p] = y - ((double) vy / VectorSymbolData.VSS_HEIGHT) * ch; double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
double sx = nx - ny * tanShear;
double sy = ny;
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
px[p] = x + rx;
py[p] = y + ry;
ipx[p] = (int) Math.round(px[p]); ipx[p] = (int) Math.round(px[p]);
ipy[p] = (int) Math.round(py[p]); ipy[p] = (int) Math.round(py[p]);
} }
@@ -970,21 +1141,91 @@ public class GraphicsPlane {
} }
/** /**
* Draws raw image pixel bitmap. * Draws raw image pixel bitmap with default 1-bit depth and uncompressed format.
*/ */
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) { public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
drawImage(x, y, width, height, imageData, fgColorArgb, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
}
/**
* Draws bitmap image with support for 1-bit, 2-bit, 4-bit, 8-bit depth and RLE decompression.
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
int bitDepth, int compressionMode) {
if (imageData == null || width <= 0 || height <= 0) return; if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0]; int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) { byte[] rawData = imageData;
int rowOffset = row * bytesPerRow; if (compressionMode == GocaConstants.IMG_RLE) {
for (int col = 0; col < width; col++) { rawData = decompressGocaRle(imageData, width, height, bitDepth);
int byteIdx = rowOffset + (col / 8); }
if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0; int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
if (bit) {
setPixel(x + col, y + row, fgColor); if (depth == 1) {
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / 8);
if (byteIdx < rawData.length) {
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
}
}
}
}
} else if (depth == 2) {
int pixelsPerByte = 4;
int bytesPerRow = (width + 3) / 4;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / pixelsPerByte);
if (byteIdx < rawData.length) {
int shift = (3 - (col % 4)) * 2;
int val = (rawData[byteIdx] >> shift) & 0x03;
if (val != 0) {
int pixelColor;
switch (val) {
case 1: pixelColor = GocaConstants.GOCA_COLORS[1]; break; // Blue
case 2: pixelColor = GocaConstants.GOCA_COLORS[2]; break; // Red
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
default: pixelColor = fgColor; break;
}
setPixel(x + col, y + row, pixelColor);
}
}
}
}
} else if (depth == 4) {
int pixelsPerByte = 2;
int bytesPerRow = (width + 1) / 2;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / pixelsPerByte);
if (byteIdx < rawData.length) {
int shift = (1 - (col % 2)) * 4;
int val = (rawData[byteIdx] >> shift) & 0x0F;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
}
}
}
}
} else if (depth == 8) {
int bytesPerRow = width;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + col;
if (byteIdx < rawData.length) {
int val = rawData[byteIdx] & 0xFF;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
}
} }
} }
} }
@@ -992,4 +1233,35 @@ public class GraphicsPlane {
hasContent = true; hasContent = true;
updateCount++; updateCount++;
} }
/**
* Decompresses IBM GOCA Run-Length Encoded (RLE) bitmap raster streams.
*/
public static byte[] decompressGocaRle(byte[] rleData, int width, int height, int bitDepth) {
if (rleData == null || rleData.length == 0) return new byte[0];
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
int bytesPerRow = (width * depth + 7) / 8;
int expectedTotalBytes = bytesPerRow * height;
byte[] out = new byte[expectedTotalBytes];
int outIdx = 0;
int inIdx = 0;
while (inIdx < rleData.length && outIdx < expectedTotalBytes) {
int count = rleData[inIdx++] & 0xFF;
if (count == 0) {
if (inIdx < rleData.length) {
int litLen = rleData[inIdx++] & 0xFF;
for (int k = 0; k < litLen && inIdx < rleData.length && outIdx < expectedTotalBytes; k++) {
out[outIdx++] = rleData[inIdx++];
}
}
} else if (inIdx < rleData.length) {
byte val = rleData[inIdx++];
for (int k = 0; k < count && outIdx < expectedTotalBytes; k++) {
out[outIdx++] = val;
}
}
}
return out;
}
} }
@@ -212,15 +212,14 @@ public class ProgramSymbolManager {
int remaining = data.length - offset; int remaining = data.length - offset;
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint; int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int bytesPerSymbol; int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (loadFormat == 1) { if (sliceBytes <= 0) sliceBytes = 18;
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
} else {
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
}
if (bytesPerSymbol <= 0) { int bytesPerSymbol;
bytesPerSymbol = 18; if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
bytesPerSymbol = sliceBytes * 3;
} else {
bytesPerSymbol = sliceBytes;
} }
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) { while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
@@ -230,10 +229,23 @@ public class ProgramSymbolManager {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length)); System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
} }
if (loadFormat == 1) { if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane); // 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
}
} else { } else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane); if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
} }
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane)); set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.listener;
/**
* Listener interface for receiving inbound SCS (SNA Character String) data streams
* transmitted by the host in TN3270E mode (Data Type DT_SCS_DATA = 0x01).
*/
public interface SCSInboundListener {
/**
* Called when an inbound SCS record is received from the host.
*
* @param data Raw SCS record payload bytes
* @param offset Start offset within buffer
* @param length Number of bytes in record
*/
void onSCSDataReceived(byte[] data, int offset, int length);
}
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -17,7 +17,7 @@ import java.util.logging.Logger;
/** /**
* Network Virtual Terminal (NVT) processor. * Network Virtual Terminal (NVT) processor.
* Handles ASCII / ANSI VT100 character stream processing, cursor positioning, * Handles ASCII / ANSI VT100 character stream processing, cursor positioning,
* escape sequence decoding, and NVT character/string transmission. * escape sequence decoding, terminal capability reports, and NVT transmission.
*/ */
public class NvtProcessor { public class NvtProcessor {
@@ -28,9 +28,10 @@ public class NvtProcessor {
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>(); private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
// Escape sequence parser states // Escape sequence parser states
private static final int STATE_NORMAL = 0; private static final int STATE_NORMAL = 0;
private static final int STATE_ESC = 1; private static final int STATE_ESC = 1;
private static final int STATE_CSI = 2; private static final int STATE_CSI = 2;
private static final int STATE_CHARSET = 3;
private int parseState = STATE_NORMAL; private int parseState = STATE_NORMAL;
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream(); private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
@@ -47,6 +48,21 @@ public class NvtProcessor {
private int savedCursorRow = 0; private int savedCursorRow = 0;
private int savedCursorCol = 0; private int savedCursorCol = 0;
// Scrolling margins (0-indexed, inclusive)
private int scrollTop = 0;
private int scrollBottom = -1; // -1 means default (rows - 1)
// Tab stops
private boolean[] tabStops;
// Cursor visibility
private boolean cursorVisible = true;
// Line drawing mode
private boolean lineDrawingG0 = false;
private boolean lineDrawingG1 = false;
private boolean activeCharsetG1 = false;
@FunctionalInterface @FunctionalInterface
public interface OutputSender { public interface OutputSender {
void sendRaw(byte[] data) throws IOException; void sendRaw(byte[] data) throws IOException;
@@ -55,6 +71,15 @@ public class NvtProcessor {
public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) { public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) {
this.screenBuffer = screenBuffer; this.screenBuffer = screenBuffer;
this.translator = translator; this.translator = translator;
initTabStops();
}
private void initTabStops() {
int cols = screenBuffer.getCols();
tabStops = new boolean[cols];
for (int i = 0; i < cols; i++) {
tabStops[i] = (i % 8 == 0);
}
} }
public void setOutputSender(OutputSender outputSender) { public void setOutputSender(OutputSender outputSender) {
@@ -65,6 +90,14 @@ public class NvtProcessor {
screenListeners.add(l); screenListeners.add(l);
} }
public void removeScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.remove(l);
}
public boolean isCursorVisible() {
return cursorVisible;
}
/** /**
* Process incoming ASCII NVT data bytes. * Process incoming ASCII NVT data bytes.
*/ */
@@ -73,6 +106,12 @@ public class NvtProcessor {
int rows = screenBuffer.getRows(); int rows = screenBuffer.getRows();
int cols = screenBuffer.getCols(); int cols = screenBuffer.getCols();
if (tabStops == null || tabStops.length != cols) {
initTabStops();
}
int effectiveScrollBottom = (scrollBottom >= 0 && scrollBottom < rows) ? scrollBottom : rows - 1;
int effectiveScrollTop = Math.max(0, Math.min(scrollTop, effectiveScrollBottom));
int size = rows * cols; int size = rows * cols;
int curAddr = screenBuffer.getCursorAddress(); int curAddr = screenBuffer.getCursorAddress();
@@ -90,27 +129,60 @@ public class NvtProcessor {
} else if (b == 0x0A) { // LF } else if (b == 0x0A) { // LF
int r = curAddr / cols; int r = curAddr / cols;
int c = curAddr % cols; int c = curAddr % cols;
r++; if (r == effectiveScrollBottom) {
if (r >= rows) { scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
scrollUp(); } else if (r < rows - 1) {
r = rows - 1; r++;
} }
curAddr = r * cols + c; curAddr = r * cols + c;
} else if (b == 0x08 || b == 0x7F) { // BS or DEL } else if (b == 0x08) { // BS
int c = curAddr % cols; int c = curAddr % cols;
if (c > 0) { if (c > 0) {
curAddr--; curAddr--;
} }
} else if (b == 0x7F) { // DEL
// Ignore or backspace per NVT convention
} else if (b == 0x09) { // TAB } else if (b == 0x09) { // TAB
int r = curAddr / cols;
int c = curAddr % cols; int c = curAddr % cols;
int nextTab = ((c / 8) + 1) * 8; int nextTab = cols - 1;
if (nextTab >= cols) nextTab = cols - 1; for (int tc = c + 1; tc < cols; tc++) {
curAddr = (curAddr / cols) * cols + nextTab; if (tc < tabStops.length && tabStops[tc]) {
nextTab = tc;
break;
}
}
curAddr = r * cols + nextTab;
} else if (b == 0x0C) { // FF } else if (b == 0x0C) { // FF
screenBuffer.clear(); screenBuffer.clear();
curAddr = 0; curAddr = 0;
} else if (b >= 0x20 && b < 0xFF) { // Printable ASCII } else if (b == 0x07) { // BEL
for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm();
}
} else if (b == 0x0E) { // SO (Select G1 charset)
activeCharsetG1 = true;
} else if (b == 0x0F) { // SI (Select G0 charset)
activeCharsetG1 = false;
} else if (b >= 0x20 && b <= 0xFF) { // Printable character
char ch = (char) b; char ch = (char) b;
if (activeCharsetG1 ? lineDrawingG1 : lineDrawingG0) {
ch = mapVt100SpecialGraphics(ch);
}
int r = curAddr / cols;
int c = curAddr % cols;
if (c >= cols) {
c = 0;
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
} else if (r < rows - 1) {
r++;
}
curAddr = r * cols + c;
}
int ebc = translator.unicodeToEbcdic(ch); int ebc = translator.unicodeToEbcdic(ch);
ExtendedAttribute cell = screenBuffer.getCell(curAddr); ExtendedAttribute cell = screenBuffer.getCell(curAddr);
cell.clear(); cell.clear();
@@ -120,37 +192,98 @@ public class NvtProcessor {
cell.bg = currentBg; cell.bg = currentBg;
cell.gr = currentGr; cell.gr = currentGr;
curAddr++; c++;
if (curAddr >= size) { if (c >= cols) {
scrollUp(); if (r < rows - 1) {
curAddr = (rows - 1) * cols; if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0;
} else {
r++;
c = 0;
}
} else {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
c = 0;
}
} }
curAddr = r * cols + c;
} }
} else if (parseState == STATE_ESC) { } else if (parseState == STATE_ESC) {
escBuffer.write(b); escBuffer.write(b);
if (b == '[') { if (b == '[') {
parseState = STATE_CSI; parseState = STATE_CSI;
} else if (b == '7') { // Save cursor } else if (b == '(' || b == ')') {
parseState = STATE_CHARSET;
} else if (b == '7') { // DECSC - Save cursor
savedCursorRow = curAddr / cols; savedCursorRow = curAddr / cols;
savedCursorCol = curAddr % cols; savedCursorCol = curAddr % cols;
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (b == '8') { // Restore cursor } else if (b == '8') { // DECRC - Restore cursor
curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol); curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol);
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (b == 'D') { // IND - Index (down 1 line)
int r = curAddr / cols;
int c = curAddr % cols;
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
} else if (r < rows - 1) {
r++;
}
curAddr = r * cols + c;
parseState = STATE_NORMAL;
} else if (b == 'M') { // RI - Reverse Index (up 1 line)
int r = curAddr / cols;
int c = curAddr % cols;
if (r == effectiveScrollTop) {
scrollDownRegion(effectiveScrollTop, effectiveScrollBottom);
} else if (r > 0) {
r--;
}
curAddr = r * cols + c;
parseState = STATE_NORMAL;
} else if (b == 'E') { // NEL - Next Line (CR + LF)
int r = curAddr / cols;
if (r == effectiveScrollBottom) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
} else if (r < rows - 1) {
r++;
}
curAddr = r * cols;
parseState = STATE_NORMAL;
} else if (b == 'H') { // HTS - Horizontal Tab Set
int c = curAddr % cols;
if (c < tabStops.length) {
tabStops[c] = true;
}
parseState = STATE_NORMAL;
} else if (b == 'c') { // RIS - Reset to Initial State } else if (b == 'c') { // RIS - Reset to Initial State
screenBuffer.clear(); screenBuffer.clear();
curAddr = 0; curAddr = 0;
currentFg = 0; currentFg = 0;
currentBg = 0; currentBg = 0;
currentGr = 0; currentGr = 0;
scrollTop = 0;
scrollBottom = rows - 1;
cursorVisible = true;
initTabStops();
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else { } else {
// Unknown 2-byte escape, finish // Unknown 2-byte escape, return to normal
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} }
} else if (parseState == STATE_CHARSET) {
byte[] seq = escBuffer.toByteArray();
if (seq.length >= 2) {
boolean isG1 = (seq[1] == ')');
boolean isLineDraw = (b == '0');
if (isG1) lineDrawingG1 = isLineDraw;
else lineDrawingG0 = isLineDraw;
}
parseState = STATE_NORMAL;
} else if (parseState == STATE_CSI) { } else if (parseState == STATE_CSI) {
escBuffer.write(b); escBuffer.write(b);
// CSI parameter/intermediate bytes: 0x20..0x3F, final bytes: 0x40..0x7E // CSI final bytes are in the range 0x40..0x7E
if (b >= 0x40 && b <= 0x7E) { if (b >= 0x40 && b <= 0x7E) {
byte[] seq = escBuffer.toByteArray(); byte[] seq = escBuffer.toByteArray();
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols); curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
@@ -159,7 +292,7 @@ public class NvtProcessor {
} }
} }
screenBuffer.setCursorAddress(curAddr); screenBuffer.setCursorAddress(Math.max(0, Math.min(size - 1, curAddr)));
screenBuffer.markAllChanged(); screenBuffer.markAllChanged();
screenBuffer.updateDisplaySnapshot(); screenBuffer.updateDisplaySnapshot();
notifyScreenUpdated(); notifyScreenUpdated();
@@ -175,6 +308,9 @@ public class NvtProcessor {
String paramStr = new String(seq, 2, seq.length - 3, StandardCharsets.US_ASCII); String paramStr = new String(seq, 2, seq.length - 3, StandardCharsets.US_ASCII);
String[] params = paramStr.split(";"); String[] params = paramStr.split(";");
int effectiveScrollBottom = (scrollBottom >= 0 && scrollBottom < rows) ? scrollBottom : rows - 1;
int effectiveScrollTop = Math.max(0, Math.min(scrollTop, effectiveScrollBottom));
int r = curAddr / cols; int r = curAddr / cols;
int c = curAddr % cols; int c = curAddr % cols;
@@ -212,6 +348,31 @@ public class NvtProcessor {
c = Math.max(0, c - count); c = Math.max(0, c - count);
return r * cols + c; return r * cols + c;
} }
case 'E': // CNL - Cursor Next Line
{
int count = parseParam(params, 0, 1);
r = Math.min(rows - 1, r + count);
return r * cols; // column 0
}
case 'F': // CPL - Cursor Previous Line
{
int count = parseParam(params, 0, 1);
r = Math.max(0, r - count);
return r * cols; // column 0
}
case 'G': // CHA - Cursor Horizontal Absolute
case '`': // HPA - Horizontal Position Absolute
{
int p = parseParam(params, 0, 1) - 1;
c = Math.max(0, Math.min(cols - 1, p));
return r * cols + c;
}
case 'd': // VPA - Vertical Position Absolute
{
int p = parseParam(params, 0, 1) - 1;
r = Math.max(0, Math.min(rows - 1, p));
return r * cols + c;
}
case 'J': // ED - Erase in Display case 'J': // ED - Erase in Display
{ {
int mode = parseParam(params, 0, 0); int mode = parseParam(params, 0, 0);
@@ -239,6 +400,84 @@ public class NvtProcessor {
} }
return curAddr; return curAddr;
} }
case 'L': // IL - Insert Line
{
int count = parseParam(params, 0, 1);
for (int n = 0; n < count; n++) {
scrollDownRegion(r, effectiveScrollBottom);
}
return r * cols;
}
case 'M': // DL - Delete Line
{
int count = parseParam(params, 0, 1);
for (int n = 0; n < count; n++) {
scrollUpRegion(r, effectiveScrollBottom);
}
return r * cols;
}
case '@': // ICH - Insert Character
{
int count = parseParam(params, 0, 1);
int lineStart = r * cols;
for (int col = cols - 1; col >= c + count; col--) {
screenBuffer.getCell(lineStart + col).copyFrom(screenBuffer.getCell(lineStart + col - count));
}
for (int col = c; col < Math.min(cols, c + count); col++) {
clearCell(lineStart + col);
}
return curAddr;
}
case 'P': // DCH - Delete Character
{
int count = parseParam(params, 0, 1);
int lineStart = r * cols;
for (int col = c; col < cols - count; col++) {
screenBuffer.getCell(lineStart + col).copyFrom(screenBuffer.getCell(lineStart + col + count));
}
for (int col = cols - count; col < cols; col++) {
clearCell(lineStart + col);
}
return curAddr;
}
case 'X': // ECH - Erase Character
{
int count = parseParam(params, 0, 1);
int end = Math.min((r + 1) * cols, curAddr + count);
for (int i = curAddr; i < end; i++) {
clearCell(i);
}
return curAddr;
}
case 'S': // SU - Scroll Up
{
int count = parseParam(params, 0, 1);
for (int n = 0; n < count; n++) {
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
}
return curAddr;
}
case 'T': // SD - Scroll Down
{
int count = parseParam(params, 0, 1);
for (int n = 0; n < count; n++) {
scrollDownRegion(effectiveScrollTop, effectiveScrollBottom);
}
return curAddr;
}
case 'r': // DECSTBM - Set Top and Bottom Margins (Scrolling Region)
{
int top = parseParam(params, 0, 1) - 1;
int bottom = parseParam(params, 1, rows) - 1;
if (top >= 0 && bottom < rows && top < bottom) {
scrollTop = top;
scrollBottom = bottom;
} else {
scrollTop = 0;
scrollBottom = rows - 1;
}
return 0; // Move cursor to home
}
case 'm': // SGR - Select Graphic Rendition case 'm': // SGR - Select Graphic Rendition
{ {
if (params.length == 0 || (params.length == 1 && params[0].isEmpty())) { if (params.length == 0 || (params.length == 1 && params[0].isEmpty())) {
@@ -256,6 +495,57 @@ public class NvtProcessor {
} }
return curAddr; return curAddr;
} }
case 'n': // DSR - Device Status Report
{
int code = parseParam(params, 0, 0);
if (code == 6) { // Cursor position request
// Reply: ESC [ <row> ; <col> R (1-indexed)
String response = String.format("\u001B[%d;%dR", r + 1, c + 1);
sendResponseString(response);
} else if (code == 5) { // Status report request
sendResponseString("\u001B[0n"); // OK
}
return curAddr;
}
case 'c': // DA - Device Attributes
{
int code = parseParam(params, 0, 0);
if (code == 0) {
// Identify as standard VT100 with Advanced Video Option
sendResponseString("\u001B[?1;2c");
}
return curAddr;
}
case 'g': // TBC - Tab Clear
{
int mode = parseParam(params, 0, 0);
if (mode == 0) {
if (c < tabStops.length) tabStops[c] = false;
} else if (mode == 3) {
Arrays.fill(tabStops, false);
}
return curAddr;
}
case 'h': // Set Mode / Private Mode
{
if (paramStr.startsWith("?")) {
String sub = paramStr.substring(1).trim();
if ("25".equals(sub)) {
cursorVisible = true;
}
}
return curAddr;
}
case 'l': // Reset Mode / Private Mode
{
if (paramStr.startsWith("?")) {
String sub = paramStr.substring(1).trim();
if ("25".equals(sub)) {
cursorVisible = false;
}
}
return curAddr;
}
case 's': // Save cursor case 's': // Save cursor
savedCursorRow = r; savedCursorRow = r;
savedCursorCol = c; savedCursorCol = c;
@@ -269,10 +559,22 @@ public class NvtProcessor {
} }
} }
private void sendResponseString(String s) {
if (outputSender != null) {
try {
outputSender.sendRaw(s.getBytes(StandardCharsets.US_ASCII));
} catch (IOException e) {
log.warning("Failed to send ANSI response: " + e.getMessage());
}
}
}
private int parseParam(String[] params, int idx, int defaultVal) { private int parseParam(String[] params, int idx, int defaultVal) {
if (params != null && idx < params.length && !params[idx].trim().isEmpty()) { if (params != null && idx < params.length && !params[idx].trim().isEmpty()) {
try { try {
return Integer.parseInt(params[idx].trim()); String val = params[idx].trim();
if (val.startsWith("?")) val = val.substring(1);
return Integer.parseInt(val);
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
return defaultVal; return defaultVal;
@@ -281,27 +583,73 @@ public class NvtProcessor {
private void clearCell(int addr) { private void clearCell(int addr) {
ExtendedAttribute cell = screenBuffer.getCell(addr); ExtendedAttribute cell = screenBuffer.getCell(addr);
cell.clear(); cell.clear();
cell.ec = 0; cell.ec = (byte) 0x40; // EBCDIC space
cell.ucs4 = ' '; cell.ucs4 = ' ';
cell.fg = 0;
cell.bg = 0;
cell.gr = 0;
} }
private void scrollUp() { private void scrollUpRegion(int top, int bottom) {
int rows = screenBuffer.getRows(); int rows = screenBuffer.getRows();
int cols = screenBuffer.getCols(); int cols = screenBuffer.getCols();
for (int r = 0; r < rows - 1; r++) { top = Math.max(0, Math.min(rows - 1, top));
bottom = Math.max(top, Math.min(rows - 1, bottom));
for (int r = top; r < bottom; r++) {
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
int dst = r * cols + c; int dst = r * cols + c;
int src = (r + 1) * cols + c; int src = (r + 1) * cols + c;
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src)); screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
} }
} }
// Clear last line int lastRowStart = bottom * cols;
int lastRowStart = (rows - 1) * cols;
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
clearCell(lastRowStart + c); clearCell(lastRowStart + c);
} }
} }
private void scrollDownRegion(int top, int bottom) {
int rows = screenBuffer.getRows();
int cols = screenBuffer.getCols();
top = Math.max(0, Math.min(rows - 1, top));
bottom = Math.max(top, Math.min(rows - 1, bottom));
for (int r = bottom; r > top; r--) {
for (int c = 0; c < cols; c++) {
int dst = r * cols + c;
int src = (r - 1) * cols + c;
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
}
}
int topRowStart = top * cols;
for (int c = 0; c < cols; c++) {
clearCell(topRowStart + c);
}
}
private char mapVt100SpecialGraphics(char c) {
switch (c) {
case 'j': return '┘';
case 'k': return '┐';
case 'l': return '┌';
case 'm': return '└';
case 'n': return '┼';
case 'q': return '─';
case 't': return '├';
case 'u': return '┤';
case 'v': return '┴';
case 'w': return '┬';
case 'x': return '│';
case '`': return '◆';
case 'a': return '▒';
case 'f': return '°';
case 'g': return '±';
case '~': return '•';
default: return c;
}
}
private void applySgr(int code) { private void applySgr(int code) {
switch (code) { switch (code) {
case 0: // Reset case 0: // Reset
@@ -204,44 +204,52 @@ public final class DS3270Constants {
public static final int SF_DESTROY_PART = 0x0d; public static final int SF_DESTROY_PART = 0x0d;
public static final int SF_ACTIVATE_PART = 0x0e; public static final int SF_ACTIVATE_PART = 0x0e;
public static final int SF_MODIFY_PART = 0x0f; public static final int SF_MODIFY_PART = 0x0f;
public static final int SF_SET_WINDOW = 0x0f; // Set Window (GOCA/Modify Partition)
public static final int SF_3270_GRAPHICS = 0x20; // 3270 Graphics / Object Control
public static final int SF_OBJECT_CONTROL = 0x20; // Object Control
public static final int SF_DOCUMENT_DATA = 0x24; // Document Data (embedded SCS / GOCA)
public static final int SF_DOC_DATA = 0x24; // Alias for Document Data
public static final int SF_OUTBOUND_DS = 0x40; public static final int SF_OUTBOUND_DS = 0x40;
public static final int SF_TRANSFER_DATA = 0xd0; public static final int SF_TRANSFER_DATA = 0xd0;
// ========== Query Reply codes ========== // ========== Query Reply codes ==========
public static final int QR_SUMMARY = 0x80; // Summary public static final int QR_SUMMARY = 0x80; // Summary
public static final int QR_USABLE_AREA = 0x81; // Usable Area public static final int QR_USABLE_AREA = 0x81; // Usable Area
public static final int QR_IMAGE = 0x82; // Image (non-GOCA) public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
public static final int QR_TEXT_PART = 0x83; // Text Partitions public static final int QR_TEXT_PART = 0x83; // Text Partitions
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
public static final int QR_CHARSETS = 0x85; // Character Sets public static final int QR_CHARSETS = 0x85; // Character Sets
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
public static final int QR_REPLY_MODES = 0x88; // Reply Modes public static final int QR_REPLY_MODES = 0x88; // Reply Modes
public static final int QR_OUTLINING = 0x8c; // Field Outlining public static final int QR_OUTLINING = 0x8c; // Field Outlining
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
public static final int QR_DDM = 0x95; // Distributed Data Management public static final int QR_DDM = 0x95; // Distributed Data Management
public static final int QR_AUXDA = 0x99; // Auxiliary Devices public static final int QR_AUXDA = 0x99; // Auxiliary Devices
public static final int QR_FILE = 0x9f; // File Transfer public static final int QR_FILE = 0x9f; // File Transfer
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer) public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy) public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8 public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics) public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA) public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev) public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format) public static final int QR_AUX_DEVICE = 0xb2; // Aux Device alias
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR public static final int QR_OEM_FORMAT = 0xb3; // OEM Format alias
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported) public static final int QR_GRAPHIC_COLOR = 0xb4; // Graphic Color alias
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
// ========== Screen model sizes ========== // ========== Screen model sizes ==========
public static final int MODEL_2_ROWS = 24; public static final int MODEL_2_ROWS = 24;
@@ -115,4 +115,23 @@ public final class TelnetConstants {
default: return "CMD-" + cmd; default: return "CMD-" + cmd;
} }
} }
public static String qualifierName(int qual) {
switch (qual) {
case TELQUAL_IS: return "IS";
case TELQUAL_SEND: return "SEND";
case TELQUAL_INFO: return "INFO";
default: return "QUAL-" + qual;
}
}
/** NEW-ENVIRON object name lookup. */
public static String environObjectName(int obj) {
switch (obj) {
case TELOBJ_VAR: return "VAR";
case TELOBJ_VALUE: return "VALUE";
case TELOBJ_ESC: return "ESC";
case TELOBJ_USERVAR: return "USERVAR";
default: return "OBJ-" + obj;
}
}
} }
@@ -18,6 +18,20 @@ public class ExtendedAttribute {
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */ /** Background color (0x00 for default, or 0xf0-0xff for explicit). */
public byte bg; public byte bg;
// Character set constants
public static final byte CS_BASE = 0;
public static final byte CS_APL = 1;
public static final byte CS_LINEDRAW = 2;
public static final byte CS_DBCS = 3;
public static final byte CS_GE = 0x04;
// DBCS state constants
public static final byte DB_NONE = 0;
public static final byte DB_LEFT = 1; // Left / first half of double-byte char
public static final byte DB_RIGHT = 2; // Right / second half of double-byte char
public static final byte DB_SI = 3; // Shift-In control char
public static final byte DB_SO = 4; // Shift-Out control char
/** /**
* Graphics rendition bits. * Graphics rendition bits.
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08 * GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.screen;
import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator; import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLPS;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -39,6 +42,14 @@ public class ScreenBuffer {
private byte defaultGr = 0x00; private byte defaultGr = 0x00;
private byte defaultCs = 0x00; private byte defaultCs = 0x00;
private byte defaultIc = 0x00; private byte defaultIc = 0x00;
// Entry Assist / DOC mode state
private boolean docMode = false;
private boolean wordWrap = false;
private int docStartCol = 0;
private int docEndCol = -1;
private int[] tabStops = null;
private final EbcdicTranslator translator; private final EbcdicTranslator translator;
private final Object renderLock = new Object(); private final Object renderLock = new Object();
@@ -216,6 +227,10 @@ public class ScreenBuffer {
this.explicitPartitionActive = (pid != 0); this.explicitPartitionActive = (pid != 0);
} }
public synchronized PartitionInfo getPartition(int pid) {
return partitions.get(pid);
}
public synchronized void eraseReset(boolean alt) { public synchronized void eraseReset(boolean alt) {
partitions.clear(); partitions.clear();
this.activePartition = 0; this.activePartition = 0;
@@ -269,7 +284,7 @@ public class ScreenBuffer {
public void setFieldAttribute(int pos, byte fa) { public void setFieldAttribute(int pos, byte fa) {
ExtendedAttribute ea = buffer[pos]; ExtendedAttribute ea = buffer[pos];
ea.clear(); ea.clear();
ea.fa = fa; ea.fa = (fa != 0) ? fa : (byte) FA_PRINTABLE;
if (!formatted) { if (!formatted) {
System.err.println("SCREEN BECAME FORMATTED at pos " + pos); System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
} }
@@ -471,39 +486,10 @@ public class ScreenBuffer {
} }
private char getAplGraphic(int ec) { private char getAplGraphic(int ec) {
switch (ec) { if (translator != null) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL) return translator.mapAPL(ec);
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return translator.ebcdicToUnicode(ec);
} }
return (char) (ec & 0xFF);
} }
/** /**
@@ -567,4 +553,511 @@ public class ScreenBuffer {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0; if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].fa; return buffer[baddr].fa;
} }
// ========== Phase 3: Field Management & Navigation ==========
/**
* Construct an ECLFieldList representation of the presentation space.
*/
public synchronized ECLFieldList buildFieldList() {
return new ECLFieldList(new ECLPS(this, null, translator), this);
}
/**
* Find the ECLField at the specified row and column.
*/
public ECLField findFieldAt(int row, int col) {
return buildFieldList().findFieldAt(row, col);
}
/**
* Find the ECLField containing the specified buffer position.
*/
public ECLField findFieldAt(int pos) {
return buildFieldList().findFieldAt(pos);
}
/**
* Find the ECLField containing the specified buffer position.
*/
public ECLField findField(int pos) {
return buildFieldList().findField(pos);
}
/**
* Find the field preceding the field at the given position.
*/
public ECLField findPrevField(int pos) {
return buildFieldList().findPrevField(pos);
}
/**
* Find the field succeeding the field at the given position.
*/
public ECLField findNextField(int pos) {
return buildFieldList().findNextField(pos);
}
/**
* Get the first field in the presentation space.
*/
public ECLField getFirstField() {
return buildFieldList().getFirstField();
}
// ========== Presentation Space Accessors & Convenience Methods ==========
public int getSize() {
return rows * cols;
}
public synchronized char getChar(int row, int col) {
int addr = rowColToAddress(row, col);
if (addr < 0 || addr >= rows * cols) return ' ';
ExtendedAttribute ea = buffer[addr];
if (ea.isFieldAttribute()) return ' ';
if (ea.ucs4 != 0) return (char) ea.ucs4;
if (ea.ec != 0) return translator.ebcdicToUnicode(ea.ec & 0xFF);
return ' ';
}
public synchronized void setChar(int row, int col, char c) {
int addr = rowColToAddress(row, col);
if (addr < 0 || addr >= rows * cols) return;
int ebc = translator.unicodeToEbcdic(c);
buffer[addr].ec = (byte) (ebc >= 0 ? ebc : 0);
buffer[addr].ucs4 = c;
screenChanged = true;
}
public synchronized void writeChar(int pos, byte ebc) {
if (pos < 0 || pos >= rows * cols) return;
buffer[pos].ec = ebc;
buffer[pos].ucs4 = translator.ebcdicToUnicode(ebc & 0xFF);
screenChanged = true;
}
public byte getAttr(int row, int col) {
return getFieldAttributeAt(rowColToAddress(row, col));
}
public ExtendedAttribute getExtAttr(int row, int col) {
return getCell(rowColToAddress(row, col));
}
public synchronized void setExtAttr(int row, int col, ExtendedAttribute ea) {
int addr = rowColToAddress(row, col);
if (addr >= 0 && addr < rows * cols && ea != null) {
buffer[addr].copyFrom(ea);
screenChanged = true;
}
}
public synchronized String getText() {
int size = rows * cols;
char[] buf = new char[size];
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (ea.isFieldAttribute()) {
buf[i] = ' ';
} else if (ea.ucs4 != 0) {
buf[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
buf[i] = ' ';
}
}
return new String(buf);
}
public synchronized String getString(int pos, int len) {
if (len <= 0) return "";
int size = rows * cols;
if (size <= 0) return "";
char[] buf = new char[len];
for (int i = 0; i < len; i++) {
int addr = (pos + i) % size;
ExtendedAttribute ea = buffer[addr];
if (ea.isFieldAttribute()) {
buf[i] = ' ';
} else if (ea.ucs4 != 0) {
buf[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
buf[i] = ' ';
}
}
return new String(buf);
}
public synchronized void setText(String text) {
if (text == null) return;
int size = rows * cols;
int len = Math.min(text.length(), size);
for (int i = 0; i < len; i++) {
char ch = text.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
buffer[i].ucs4 = ch;
}
screenChanged = true;
updateDisplaySnapshot();
}
public int searchString(String target) {
if (target == null || target.isEmpty()) return -1;
String full = getText();
return full.indexOf(target);
}
public boolean isModified() {
int size = rows * cols;
for (int i = 0; i < size; i++) {
if (buffer[i].isFieldAttribute() && faIsModified(buffer[i].fa & 0xFF)) {
return true;
}
}
return false;
}
public boolean isModified(int pos) {
return faIsModified(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isProtected(int pos) {
return faIsProtected(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isNumeric(int pos) {
return faIsNumeric(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isDisplay(int pos) {
return !faIsZero(getFieldAttributeAt(pos) & 0xFF);
}
// ========== DBCS Character Insertion & Deletion ==========
/**
* Insert a character at the specified buffer address with field boundary and DBCS preservation.
*/
public synchronized boolean insertChar(int pos, char ch) {
int size = rows * cols;
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (formatted) {
byte faVal = getFieldAttributeAt(pos);
if (faIsProtected(faVal & 0xFF)) return false;
}
boolean isDbcsChar = translator != null && translator.isDBCS() && translator.unicodeToDbcs(ch) >= 0;
int shiftAmount = isDbcsChar ? 2 : 1;
// Find end of field
int endAddr = pos;
int count = 0;
while (!getCell(incrementAddress(endAddr)).isFieldAttribute() && count < size) {
endAddr = incrementAddress(endAddr);
count++;
if (endAddr == pos) break;
}
// Check overflow
for (int s = 0; s < shiftAmount; s++) {
int checkAddr = endAddr;
for (int k = 0; k < s; k++) checkAddr = decrementAddress(checkAddr);
ExtendedAttribute eaEnd = getCell(checkAddr);
if (eaEnd.ec != 0 && eaEnd.ec != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
return false;
}
}
// Shift characters right
for (int s = 0; s < shiftAmount; s++) {
int dst = endAddr;
int shiftCount = 0;
while (dst != pos && shiftCount < size) {
int src = decrementAddress(dst);
getCell(dst).copyFrom(getCell(src));
dst = src;
shiftCount++;
}
getCell(pos).clear();
}
if (isDbcsChar) {
int dbcs = translator.unicodeToDbcs(ch);
int b1 = (dbcs >> 8) & 0xFF;
int b2 = dbcs & 0xFF;
int nextPos = incrementAddress(pos);
ExtendedAttribute ea1 = getCell(pos);
ea1.ec = (byte) b1;
ea1.ucs4 = ch;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ExtendedAttribute ea2 = getCell(nextPos);
ea2.ec = (byte) b2;
ea2.ucs4 = ch;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
setCursorAddress(incrementAddress(nextPos));
} else {
int ebc = translator.unicodeToEbcdic(ch);
ExtendedAttribute ea = getCell(pos);
ea.ec = (byte) (ebc >= 0 ? ebc : 0);
ea.ucs4 = ch;
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_NONE;
setCursorAddress(incrementAddress(pos));
}
// Set MDT
if (formatted) {
int faAddr = findFieldAttribute(pos);
if (faAddr >= 0) {
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
}
}
screenChanged = true;
updateDisplaySnapshot();
return true;
}
public synchronized boolean insertChar(char ch) {
return insertChar(cursorAddress, ch);
}
/**
* Delete a character at the specified buffer address, pulling trailing field text and preserving DBCS glyphs.
*/
public synchronized boolean deleteChar(int pos) {
int size = rows * cols;
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (getCell(pos).isFieldAttribute()) return false;
if (formatted) {
byte faVal = getFieldAttributeAt(pos);
if (faIsProtected(faVal & 0xFF)) return false;
}
ExtendedAttribute curCell = getCell(pos);
boolean isDbcs = curCell.db == ExtendedAttribute.DB_LEFT || curCell.db == ExtendedAttribute.DB_RIGHT
|| curCell.cs == ExtendedAttribute.CS_DBCS;
int deleteAmount = isDbcs ? 2 : 1;
if (curCell.db == ExtendedAttribute.DB_RIGHT) {
pos = decrementAddress(pos);
}
for (int d = 0; d < deleteAmount; d++) {
int shiftAddr = pos;
int count = 0;
while (count < size) {
int next = incrementAddress(shiftAddr);
if (getCell(next).isFieldAttribute()) {
getCell(shiftAddr).clear();
break;
}
getCell(shiftAddr).copyFrom(getCell(next));
shiftAddr = next;
count++;
}
}
cleanAdjacentSISO(pos);
if (formatted) {
int faAddr = findFieldAttribute(pos);
if (faAddr >= 0) {
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
}
}
screenChanged = true;
updateDisplaySnapshot();
return true;
}
public synchronized boolean deleteChar() {
return deleteChar(cursorAddress);
}
private void cleanAdjacentSISO(int nearPos) {
int size = rows * cols;
int start = Math.max(0, nearPos - 5);
int end = Math.min(size, nearPos + 10);
for (int i = start; i < end - 1; i++) {
ExtendedAttribute ea1 = getCell(i);
ExtendedAttribute ea2 = getCell(i + 1);
if (!ea1.isFieldAttribute() && !ea2.isFieldAttribute()) {
if ((ea1.ec & 0xFF) == 0x0E && (ea2.ec & 0xFF) == 0x0F) {
ea1.clear();
ea2.clear();
}
}
}
}
// ========== Entry Assist & DOC Mode Operations ==========
public boolean isEntryAssistDOCmode() { return docMode; }
public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; }
public boolean isEntryAssistWordWrap() { return wordWrap; }
public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; }
public int getEntryAssistStartColumn() { return docStartCol; }
public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); }
public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); }
public void setEntryAssistEndColumn(int n) { this.docEndCol = n; }
public int[] getEntryAssistTabStops() { return tabStops; }
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
public synchronized void processWordTab(boolean forward) {
int size = rows * cols;
if (size <= 0) return;
int cur = cursorAddress;
int curRow = cur / cols;
int curCol = cur % cols;
if (tabStops != null && tabStops.length > 0) {
if (forward) {
for (int stop : tabStops) {
if (stop > curCol && stop < cols) {
setCursorPosition(curRow, stop);
return;
}
}
int nextRow = (curRow + 1) % rows;
setCursorPosition(nextRow, tabStops[0]);
return;
} else {
for (int i = tabStops.length - 1; i >= 0; i--) {
int stop = tabStops[i];
if (stop < curCol && stop >= 0) {
setCursorPosition(curRow, stop);
return;
}
}
int prevRow = (curRow - 1 + rows) % rows;
setCursorPosition(prevRow, tabStops[tabStops.length - 1]);
return;
}
}
// Standard Word Tab: Jump to next / prev word boundary
if (forward) {
int addr = cur;
int count = 0;
while (count < size && getChar(addr / cols, addr % cols) != ' ' && !getCell(addr).isFieldAttribute()) {
addr = incrementAddress(addr);
count++;
}
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
addr = incrementAddress(addr);
count++;
}
setCursorAddress(addr);
} else {
int addr = decrementAddress(cur);
int count = 0;
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
addr = decrementAddress(addr);
count++;
}
while (count < size && getChar(decrementAddress(addr) / cols, decrementAddress(addr) % cols) != ' '
&& !getCell(decrementAddress(addr)).isFieldAttribute()) {
addr = decrementAddress(addr);
count++;
}
setCursorAddress(addr);
}
updateDisplaySnapshot();
}
public synchronized void processDeleteWord() {
int size = rows * cols;
if (size <= 0) return;
int cur = cursorAddress;
if (formatted) {
byte fa = getFieldAttributeAt(cur);
if (faIsProtected(fa & 0xFF)) return;
}
int endWord = cur;
int count = 0;
while (count < size && !getCell(endWord).isFieldAttribute()) {
char ch = getChar(endWord / cols, endWord % cols);
endWord = incrementAddress(endWord);
count++;
if (ch == ' ') break;
}
for (int i = 0; i < count; i++) {
deleteChar(cur);
}
screenChanged = true;
updateDisplaySnapshot();
}
// ========== DBCS Shift-Out / Shift-In Display Transformation ==========
public synchronized void processSOSI() {
int size = rows * cols;
if (size <= 0) return;
boolean insideDBCS = false;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (ea.isFieldAttribute()) {
insideDBCS = false;
continue;
}
int ec = ea.ec & 0xFF;
if (ec == 0x0E) { // Shift-Out
insideDBCS = true;
ea.db = ExtendedAttribute.DB_SO;
ea.ucs4 = ' ';
} else if (ec == 0x0F) { // Shift-In
insideDBCS = false;
ea.db = ExtendedAttribute.DB_SI;
ea.ucs4 = ' ';
} else if (insideDBCS) {
int nextIdx = (i + 1) % size;
ExtendedAttribute nextEa = buffer[nextIdx];
if (!nextEa.isFieldAttribute() && (nextEa.ec & 0xFF) != 0x0F) {
int b1 = ec;
int b2 = nextEa.ec & 0xFF;
ea.cs = ExtendedAttribute.CS_DBCS;
ea.db = ExtendedAttribute.DB_LEFT;
nextEa.cs = ExtendedAttribute.CS_DBCS;
nextEa.db = ExtendedAttribute.DB_RIGHT;
if (translator != null && translator.isDBCS() && translator.getCodePage() != null) {
char uni = translator.getCodePage().dbcsToUnicode(b1, b2);
ea.ucs4 = uni;
nextEa.ucs4 = uni;
}
i++;
}
} else {
ea.db = ExtendedAttribute.DB_NONE;
if (ea.cs == ExtendedAttribute.CS_DBCS) {
ea.cs = ExtendedAttribute.CS_BASE;
}
}
}
screenChanged = true;
updateDisplaySnapshot();
}
} }
@@ -40,44 +40,71 @@ public class TelnetConnection {
} }
/** /**
* Connect to the host. Blocks until connection is established or fails. * Connect to the host (optionally through a proxy). Blocks until connection is established or fails.
*/ */
public void connect() throws IOException { public void connect() throws IOException {
ConnectionConfig.ProxyType proxyType = config.getProxyType();
boolean hasProxy = proxyType != null && proxyType != ConnectionConfig.ProxyType.NONE &&
config.getProxyHost() != null && !config.getProxyHost().trim().isEmpty();
String connectHost = hasProxy ? config.getProxyHost().trim() : config.getHost();
int connectPort = hasProxy ? (config.getProxyPort() > 0 ? config.getProxyPort() : (proxyType == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080)) : config.getPort();
log.info("Connecting TCP socket to " + connectHost + ":" + connectPort +
(hasProxy ? " (via " + proxyType + " proxy for " + config.getHost() + ":" + config.getPort() + ")" : "") +
(config.isUseTls() ? " with TLS" : ""));
Socket rawSocket = new Socket();
rawSocket.setKeepAlive(config.isSoKeepAlive());
rawSocket.setOOBInline(true);
rawSocket.setTcpNoDelay(config.isTcpNoDelay());
if (config.getSoTimeoutMs() > 0) {
rawSocket.setSoTimeout(config.getSoTimeoutMs());
}
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
// Perform proxy handshake if configured
if (hasProxy) {
switch (proxyType) {
case HTTP:
establishHttpProxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword());
break;
case SOCKS4:
establishSocks4Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername());
break;
case SOCKS5:
establishSocks5Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword());
break;
default:
break;
}
}
if (config.isUseTls()) { if (config.isUseTls()) {
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() + log.info("Performing TLS handshake with " + config.getHost() + ":" + config.getPort() +
" (verifyCert=" + config.isTlsVerifyCert() + ")"); " (verifyCert=" + config.isTlsVerifyCert() + ")");
try { try {
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config); javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory(); javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(); javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(
rawSocket, config.getHost(), config.getPort(), true);
sslSocket.setKeepAlive(config.isSoKeepAlive()); sslSocket.setKeepAlive(config.isSoKeepAlive());
sslSocket.setTcpNoDelay(config.isTcpNoDelay()); sslSocket.setTcpNoDelay(config.isTcpNoDelay());
if (config.getSoTimeoutMs() > 0) { if (config.getSoTimeoutMs() > 0) {
sslSocket.setSoTimeout(config.getSoTimeoutMs()); sslSocket.setSoTimeout(config.getSoTimeoutMs());
} }
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
sslSocket.startHandshake(); sslSocket.startHandshake();
socket = sslSocket; socket = sslSocket;
sslSession = sslSocket.getSession(); sslSession = sslSocket.getSession();
log.info("TLS session active: protocol=" + sslSession.getProtocol() + log.info("TLS session active: protocol=" + sslSession.getProtocol() +
" cipher=" + sslSession.getCipherSuite()); " cipher=" + sslSession.getCipherSuite());
} catch (IOException e) { } catch (IOException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
throw new IOException("TLS setup failure: " + e.getMessage(), e); throw new IOException("TLS setup failure: " + e.getMessage(), e);
} }
} else { } else {
log.info("Connecting to " + config.getHost() + ":" + config.getPort()); socket = rawSocket;
socket = new Socket();
socket.setKeepAlive(config.isSoKeepAlive());
socket.setOOBInline(true);
socket.setTcpNoDelay(config.isTcpNoDelay());
if (config.getSoTimeoutMs() > 0) {
socket.setSoTimeout(config.getSoTimeoutMs());
}
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
} }
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE); inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
@@ -91,6 +118,235 @@ public class TelnetConnection {
readerThread.start(); readerThread.start();
} }
/**
* Dynamically elevate active socket to TLS in-band (STARTTLS / Option 46).
*/
public synchronized void upgradeToTls() throws IOException {
if (socket == null || !socket.isConnected() || socket.isClosed()) {
throw new IOException("Cannot upgrade disconnected socket to TLS");
}
log.info("Elevating active connection to TLS via STARTTLS");
try {
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(
socket, config.getHost(), config.getPort(), true);
sslSocket.setKeepAlive(config.isSoKeepAlive());
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
if (config.getSoTimeoutMs() > 0) {
sslSocket.setSoTimeout(config.getSoTimeoutMs());
}
sslSocket.startHandshake();
this.socket = sslSocket;
this.sslSession = sslSocket.getSession();
this.inputStream = new BufferedInputStream(sslSocket.getInputStream(), READ_BUFFER_SIZE);
this.outputStream = new BufferedOutputStream(sslSocket.getOutputStream());
log.info("STARTTLS session active: protocol=" + sslSession.getProtocol() +
" cipher=" + sslSession.getCipherSuite());
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException("STARTTLS setup failure: " + e.getMessage(), e);
}
}
private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
StringBuilder req = new StringBuilder();
req.append("CONNECT ").append(targetHost).append(":").append(targetPort).append(" HTTP/1.1\r\n");
req.append("Host: ").append(targetHost).append(":").append(targetPort).append("\r\n");
if (user != null && !user.isEmpty()) {
String auth = user + ":" + (pass != null ? pass : "");
String encoded = java.util.Base64.getEncoder().encodeToString(auth.getBytes(java.nio.charset.StandardCharsets.UTF_8));
req.append("Proxy-Authorization: Basic ").append(encoded).append("\r\n");
}
req.append("Proxy-Connection: Keep-Alive\r\n\r\n");
out.write(req.toString().getBytes(java.nio.charset.StandardCharsets.US_ASCII));
out.flush();
// Read HTTP status line
ByteArrayOutputStream lineBuf = new ByteArrayOutputStream();
int b;
while ((b = in.read()) != -1) {
if (b == '\n') break;
if (b != '\r') lineBuf.write(b);
}
String statusLine = new String(lineBuf.toByteArray(), java.nio.charset.StandardCharsets.US_ASCII);
if (!statusLine.contains(" 200")) {
throw new IOException("HTTP proxy connection failed: " + statusLine);
}
// Consume remaining response headers until empty line
while (true) {
lineBuf.reset();
while ((b = in.read()) != -1) {
if (b == '\n') break;
if (b != '\r') lineBuf.write(b);
}
if (lineBuf.size() == 0) break; // empty line terminates headers
}
log.info("HTTP proxy tunnel established to " + targetHost + ":" + targetPort);
}
private void establishSocks4Proxy(Socket s, String targetHost, int targetPort, String user) throws IOException {
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
byte[] ip = new byte[4];
boolean isSocks4a = false;
try {
InetAddress addr = InetAddress.getByName(targetHost);
if (addr instanceof Inet4Address) {
ip = addr.getAddress();
} else {
isSocks4a = true;
ip = new byte[] { 0, 0, 0, 1 };
}
} catch (Exception e) {
isSocks4a = true;
ip = new byte[] { 0, 0, 0, 1 };
}
ByteArrayOutputStream req = new ByteArrayOutputStream();
req.write(0x04); // SOCKS version 4
req.write(0x01); // CONNECT command
req.write((targetPort >> 8) & 0xFF);
req.write(targetPort & 0xFF);
req.write(ip);
if (user != null && !user.isEmpty()) {
req.write(user.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
}
req.write(0x00); // Null terminator for userid
if (isSocks4a) {
req.write(targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
req.write(0x00); // Null terminator for domain name
}
out.write(req.toByteArray());
out.flush();
byte[] resp = new byte[8];
int read = 0;
while (read < 8) {
int n = in.read(resp, read, 8 - read);
if (n < 0) throw new IOException("Unexpected EOF reading SOCKS4 response");
read += n;
}
int status = resp[1] & 0xFF;
if (status != 0x5A) {
throw new IOException("SOCKS4 proxy request rejected, status=0x" + Integer.toHexString(status));
}
log.info("SOCKS4 proxy tunnel established to " + targetHost + ":" + targetPort);
}
private void establishSocks5Proxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
boolean hasAuth = user != null && !user.isEmpty();
if (hasAuth) {
out.write(new byte[] { 0x05, 0x02, 0x00, 0x02 }); // SOCKS5, 2 methods: NO_AUTH(0x00), USER_PASS(0x02)
} else {
out.write(new byte[] { 0x05, 0x01, 0x00 }); // SOCKS5, 1 method: NO_AUTH(0x00)
}
out.flush();
byte[] methodResp = new byte[2];
readFully(in, methodResp);
if ((methodResp[0] & 0xFF) != 0x05) {
throw new IOException("Invalid SOCKS5 version response: " + (methodResp[0] & 0xFF));
}
int authMethod = methodResp[1] & 0xFF;
if (authMethod == 0x02) {
// RFC 1929 Username/Password Authentication
byte[] uBytes = user.getBytes(java.nio.charset.StandardCharsets.UTF_8);
byte[] pBytes = (pass != null ? pass : "").getBytes(java.nio.charset.StandardCharsets.UTF_8);
ByteArrayOutputStream authReq = new ByteArrayOutputStream();
authReq.write(0x01); // Auth subnegotiation version
authReq.write(uBytes.length);
authReq.write(uBytes);
authReq.write(pBytes.length);
authReq.write(pBytes);
out.write(authReq.toByteArray());
out.flush();
byte[] authResp = new byte[2];
readFully(in, authResp);
if (authResp[1] != 0x00) {
throw new IOException("SOCKS5 username/password authentication failed");
}
} else if (authMethod != 0x00) {
throw new IOException("SOCKS5 proxy authentication method rejected: 0x" + Integer.toHexString(authMethod));
}
// Send CONNECT request
ByteArrayOutputStream connReq = new ByteArrayOutputStream();
connReq.write(0x05); // SOCKS5
connReq.write(0x01); // CONNECT
connReq.write(0x00); // Reserved
try {
InetAddress addr = InetAddress.getByName(targetHost);
if (addr instanceof Inet4Address) {
connReq.write(0x01); // ATYP IPv4
connReq.write(addr.getAddress());
} else if (addr instanceof Inet6Address) {
connReq.write(0x04); // ATYP IPv6
connReq.write(addr.getAddress());
} else {
byte[] dBytes = targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
connReq.write(0x03); // ATYP Domain
connReq.write(dBytes.length);
connReq.write(dBytes);
}
} catch (Exception e) {
byte[] dBytes = targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
connReq.write(0x03); // ATYP Domain
connReq.write(dBytes.length);
connReq.write(dBytes);
}
connReq.write((targetPort >> 8) & 0xFF);
connReq.write(targetPort & 0xFF);
out.write(connReq.toByteArray());
out.flush();
byte[] connResp = new byte[4];
readFully(in, connResp);
int rep = connResp[1] & 0xFF;
if (rep != 0x00) {
throw new IOException("SOCKS5 connect command failed, rep=0x" + Integer.toHexString(rep));
}
int atyp = connResp[3] & 0xFF;
if (atyp == 0x01) {
byte[] bnd = new byte[4 + 2]; // IPv4 + Port
readFully(in, bnd);
} else if (atyp == 0x03) {
int len = in.read();
if (len < 0) throw new IOException("Unexpected EOF in SOCKS5 domain response");
byte[] bnd = new byte[len + 2]; // Domain + Port
readFully(in, bnd);
} else if (atyp == 0x04) {
byte[] bnd = new byte[16 + 2]; // IPv6 + Port
readFully(in, bnd);
}
log.info("SOCKS5 proxy tunnel established to " + targetHost + ":" + targetPort);
}
private static void readFully(InputStream in, byte[] buf) throws IOException {
int read = 0;
while (read < buf.length) {
int n = in.read(buf, read, buf.length - read);
if (n < 0) throw new IOException("Unexpected EOF reading proxy response");
read += n;
}
}
/** /**
* Send raw bytes to the host. * Send raw bytes to the host.
*/ */
@@ -107,19 +107,20 @@ public class TelnetFSM {
// Listeners // Listeners
private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<>(); private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<>();
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>(); private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
private final List<SCSInboundListener> scsListeners = new CopyOnWriteArrayList<>();
// Connected LU info // Connected LU info
private String connectedLu; private String connectedLu;
private String connectedType; private String connectedType;
enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP } public enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) { public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
this.config = config; this.config = config;
this.screenBuffer = screenBuffer; this.screenBuffer = screenBuffer;
this.dsProcessor = dsProcessor; this.dsProcessor = dsProcessor;
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator()); this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
this.nvtProcessor.setOutputSender(this::sendBytes); this.nvtProcessor.setOutputSender(this::sendNvtData);
} }
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() { public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
@@ -131,10 +132,25 @@ public class TelnetFSM {
} }
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); } public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
public void removeConnectionListener(ConnectionListener l) { connectionListeners.remove(l); }
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l); screenListeners.add(l);
nvtProcessor.addScreenUpdateListener(l); nvtProcessor.addScreenUpdateListener(l);
} }
public void removeScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.remove(l);
nvtProcessor.removeScreenUpdateListener(l);
}
public void addSCSInboundListener(SCSInboundListener l) {
if (l != null && !scsListeners.contains(l)) {
scsListeners.add(l);
}
}
public void removeSCSInboundListener(SCSInboundListener l) {
scsListeners.remove(l);
}
public ConnectionState getConnectionState() { return connectionState; } public ConnectionState getConnectionState() { return connectionState; }
public boolean[] getMyOpts() { return myOpts; } public boolean[] getMyOpts() { return myOpts; }
@@ -189,9 +205,11 @@ public class TelnetFSM {
if (connectionState == ConnectionState.TELNET_PENDING) { if (connectionState == ConnectionState.TELNET_PENDING) {
changeState(ConnectionState.CONNECTED_NVT); changeState(ConnectionState.CONNECTED_NVT);
} }
if (connectionState.isNvt()) { boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
if (isPlainNvt) {
nvtProcessor.processNVTData(buf, start, i - start); nvtProcessor.processNVTData(buf, start, i - start);
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) { } else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
ibuf.write(buf, start, i - start); ibuf.write(buf, start, i - start);
} }
} }
@@ -260,13 +278,15 @@ public class TelnetFSM {
changeState(ConnectionState.CONNECTED_NVT); changeState(ConnectionState.CONNECTED_NVT);
} }
if (connectionState.isNvt()) { boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
if (isPlainNvt) {
nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1); nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1);
return; return;
} }
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending) // Accumulate data for 3270, TN3270E (including CONNECTED_E_NVT, SSCP-LU and unbound states, and pending)
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) { if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
ibuf.write(c); ibuf.write(c);
} }
} }
@@ -336,6 +356,13 @@ public class TelnetFSM {
sendCommand(DO, opt); sendCommand(DO, opt);
break; break;
case TELOPT_STARTTLS:
if (config.isStartTlsEnabled() && !hisOpts[opt]) {
hisOpts[opt] = true;
sendCommand(DO, opt);
}
break;
case TELOPT_TN3270E: case TELOPT_TN3270E:
if (!config.isTn3270eEnabled()) { if (!config.isTn3270eEnabled()) {
sendCommand(DONT, opt); sendCommand(DONT, opt);
@@ -384,6 +411,13 @@ public class TelnetFSM {
sendCommand(WILL, opt); sendCommand(WILL, opt);
break; break;
case TELOPT_STARTTLS:
if (config.isStartTlsEnabled() && !myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
break;
case TELOPT_TTYPE: case TELOPT_TTYPE:
if (!myOpts[opt]) { if (!myOpts[opt]) {
myOpts[opt] = true; myOpts[opt] = true;
@@ -489,12 +523,37 @@ public class TelnetFSM {
case TELOPT_NEW_ENVIRON: case TELOPT_NEW_ENVIRON:
handleNewEnvironSB(data); handleNewEnvironSB(data);
break; break;
case TELOPT_STARTTLS:
handleStartTlsSB(data);
break;
default: default:
log.info("Ignoring SB for option " + opt); log.info("Ignoring SB for option " + opt);
break; break;
} }
} }
// ========== STARTTLS sub-negotiation ==========
private void handleStartTlsSB(byte[] data) {
if (data.length >= 2 && (data[1] & 0xFF) == TLS_FOLLOWS) {
log.info("RCVD SB STARTTLS FOLLOWS (1) - Initiating TLS elevation");
processStartTls();
}
}
public void processStartTls() {
log.info("Elevating active connection to TLS via STARTTLS (Option 46)");
try {
if (connection != null) {
connection.upgradeToTls();
statusDisplay(STATUS_SECURITY, "TLS socket elevated via STARTTLS");
}
} catch (IOException e) {
log.log(Level.SEVERE, "Failed to elevate socket to TLS via STARTTLS", e);
onError("STARTTLS elevation failed: " + e.getMessage());
}
}
// ========== TTYPE sub-negotiation ========== // ========== TTYPE sub-negotiation ==========
private void handleTTypeSB(byte[] data) { private void handleTTypeSB(byte[] data) {
@@ -522,15 +581,117 @@ public class TelnetFSM {
} }
} }
// ========== NEW_ENVIRON sub-negotiation ========== // ========== NEW_ENVIRON sub-negotiation (RFC 1572 / RFC 2877) ==========
private void handleNewEnvironSB(byte[] data) { private void handleNewEnvironSB(byte[] data) {
if (data.length >= 2 && data[1] == TELQUAL_SEND) { if (data.length < 2) return;
log.info("RCVD SB NEW-ENVIRON SEND - Responding with empty IS"); int qual = data[1] & 0xFF;
byte[] response = { (byte) IAC, (byte) SB, (byte) TELOPT_NEW_ENVIRON, if (qual == TELQUAL_SEND) {
(byte) TELQUAL_IS, (byte) IAC, (byte) SE }; log.info("RCVD SB NEW-ENVIRON SEND (" + (data.length - 2) + " bytes)");
sendBytes(response); if (data.length == 2) {
log.info("SENT SB NEW-ENVIRON IS SE"); // Empty SEND: send all configured variables
sendNewEnvironmentVariables(config.getEnvironmentVariables(), config.getUserVariables());
return;
}
// Parse requested variable names
java.util.Map<String, String> respVars = new java.util.LinkedHashMap<>();
java.util.Map<String, String> respUserVars = new java.util.LinkedHashMap<>();
int idx = 2;
while (idx < data.length) {
int objType = data[idx++] & 0xFF;
ByteArrayOutputStream nameBuf = new ByteArrayOutputStream();
boolean escaped = false;
while (idx < data.length) {
int b = data[idx] & 0xFF;
if (!escaped && (b == TELOBJ_VAR || b == TELOBJ_USERVAR)) {
break;
}
idx++;
if (!escaped && b == TELOBJ_ESC) {
escaped = true;
continue;
}
nameBuf.write(b);
escaped = false;
}
String varName = new String(nameBuf.toByteArray(), java.nio.charset.StandardCharsets.US_ASCII);
if (objType == TELOBJ_VAR) {
if (varName.isEmpty()) {
respVars.putAll(config.getEnvironmentVariables());
} else if (config.getEnvironmentVariables().containsKey(varName)) {
respVars.put(varName, config.getEnvironmentVariables().get(varName));
}
} else if (objType == TELOBJ_USERVAR) {
if (varName.isEmpty()) {
respUserVars.putAll(config.getUserVariables());
} else if (config.getUserVariables().containsKey(varName)) {
respUserVars.put(varName, config.getUserVariables().get(varName));
}
}
}
sendNewEnvironmentVariables(respVars, respUserVars);
}
}
public void sendNewEnvironmentVariables(java.util.Properties props) {
java.util.Map<String, String> vars = new java.util.LinkedHashMap<>();
java.util.Map<String, String> uVars = new java.util.LinkedHashMap<>();
if (props != null) {
for (String k : props.stringPropertyNames()) {
if (k.startsWith("USERVAR_") || k.startsWith("USER_")) {
uVars.put(k, props.getProperty(k));
} else {
vars.put(k, props.getProperty(k));
}
}
}
sendNewEnvironmentVariables(vars, uVars);
}
public void sendNewEnvironmentVariables(java.util.Map<String, String> vars, java.util.Map<String, String> userVars) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_NEW_ENVIRON);
out.write(TELQUAL_IS);
if (vars != null) {
for (java.util.Map.Entry<String, String> e : vars.entrySet()) {
out.write(TELOBJ_VAR);
writeEscapedEnvironString(out, e.getKey());
out.write(TELOBJ_VALUE);
writeEscapedEnvironString(out, e.getValue());
}
}
if (userVars != null) {
for (java.util.Map.Entry<String, String> e : userVars.entrySet()) {
out.write(TELOBJ_USERVAR);
writeEscapedEnvironString(out, e.getKey());
out.write(TELOBJ_VALUE);
writeEscapedEnvironString(out, e.getValue());
}
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB NEW-ENVIRON IS (" + ((vars != null ? vars.size() : 0) + (userVars != null ? userVars.size() : 0)) + " vars) SE");
}
private void writeEscapedEnvironString(ByteArrayOutputStream out, String s) {
if (s == null) return;
for (byte b : s.getBytes(java.nio.charset.StandardCharsets.US_ASCII)) {
int ub = b & 0xFF;
if (ub == TELOBJ_VAR || ub == TELOBJ_VALUE || ub == TELOBJ_ESC || ub == TELOBJ_USERVAR) {
out.write(TELOBJ_ESC);
} else if (ub == IAC) {
out.write(IAC);
}
out.write(ub);
} }
} }
@@ -816,8 +977,10 @@ public class TelnetFSM {
ibuf.reset(); ibuf.reset();
if (data.length == 0) return; if (data.length == 0) return;
if (connectionState == ConnectionState.TELNET_PENDING && !tn3270eNegotiated) { if ((connectionState == ConnectionState.TELNET_PENDING ||
log.info("Received EOR during TELNET_PENDING - transitioning to plain TN3270 mode"); connectionState == ConnectionState.CONNECTED_NVT ||
connectionState == ConnectionState.CONNECTED_NVT_CHAR) && !tn3270eNegotiated) {
log.info("Received EOR during NVT/pending - transitioning to plain TN3270 mode");
changeState(ConnectionState.CONNECTED_3270); changeState(ConnectionState.CONNECTED_3270);
} }
@@ -831,6 +994,10 @@ public class TelnetFSM {
} }
} }
public void processTn3270eHeader(byte[] data) {
processTN3270ERecord(data);
}
private void processTN3270ERecord(byte[] data) { private void processTN3270ERecord(byte[] data) {
if (data.length < EH_SIZE) { if (data.length < EH_SIZE) {
log.warning("TN3270E record too short: " + data.length); log.warning("TN3270E record too short: " + data.length);
@@ -850,11 +1017,10 @@ public class TelnetFSM {
switch (dataType) { switch (dataType) {
case DT_3270_DATA: case DT_3270_DATA:
if (data.length > EH_SIZE) { if (data.length > EH_SIZE) {
// Transition to 3270 mode // Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP)
if (connectionState == ConnectionState.CONNECTED_UNBOUND || if (connectionState != ConnectionState.CONNECTED_TN3270E) {
connectionState == ConnectionState.CONNECTED_SSCP) { // Clear screen on transition to 3270 mode from unbound/SSCP/NVT
// Clear screen on transition to 3270 mode from unbound/SSCP // This ensures old SSCP-LU or NVT data doesn't persist
// This ensures old SSCP-LU data or stale content doesn't persist
screenBuffer.erase(false); screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_TN3270E); changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270; tn3270eSubmode = TN3270ESubmode.E_3270;
@@ -879,9 +1045,30 @@ public class TelnetFSM {
} }
break; break;
case DT_SCS_DATA:
if (data.length > EH_SIZE) {
try {
processSCSInbound(data, EH_SIZE, data.length - EH_SIZE);
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
} catch (Exception e) {
log.log(Level.WARNING, "Error processing SCS record", e);
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
}
}
} else {
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
break;
case DT_SSCP_LU_DATA: case DT_SSCP_LU_DATA:
if (connectionState != ConnectionState.CONNECTED_SSCP) { if (connectionState != ConnectionState.CONNECTED_SSCP) {
if (connectionState == ConnectionState.CONNECTED_UNBOUND) { if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
connectionState == ConnectionState.CONNECTED_E_NVT) {
// Clear screen on first SSCP-LU transition to remove stale data // Clear screen on first SSCP-LU transition to remove stale data
screenBuffer.clear(); screenBuffer.clear();
} }
@@ -919,8 +1106,13 @@ public class TelnetFSM {
case DT_NVT_DATA: case DT_NVT_DATA:
// NVT data in TN3270E mode // NVT data in TN3270E mode
changeState(ConnectionState.CONNECTED_E_NVT); if (connectionState != ConnectionState.CONNECTED_E_NVT) {
tn3270eSubmode = TN3270ESubmode.E_NVT; changeState(ConnectionState.CONNECTED_E_NVT);
tn3270eSubmode = TN3270ESubmode.E_NVT;
}
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
}
if (data.length > EH_SIZE) { if (data.length > EH_SIZE) {
try { try {
processNVTData(data, EH_SIZE, data.length - EH_SIZE); processNVTData(data, EH_SIZE, data.length - EH_SIZE);
@@ -938,6 +1130,7 @@ public class TelnetFSM {
sendTN3270EPositiveResponse(seqNumber); sendTN3270EPositiveResponse(seqNumber);
} }
} }
notifyScreenUpdate();
break; break;
case DT_REQUEST: case DT_REQUEST:
@@ -957,6 +1150,16 @@ public class TelnetFSM {
} }
break; break;
case DT_BID:
process_BID(responseFlag, seqNumber);
break;
case DT_PRINT_EOJ:
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
break;
case DT_RESPONSE: case DT_RESPONSE:
lastRcvSeq = seqNumber; lastRcvSeq = seqNumber;
log.fine("Received response, seq=" + seqNumber); log.fine("Received response, seq=" + seqNumber);
@@ -966,8 +1169,8 @@ public class TelnetFSM {
// Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.) // Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.)
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream. // This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F || if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 || dataType == 0x05 || dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
dataType == 0x0D || dataType == 0x01 || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) { dataType == 0x0D || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) + log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
") in TN3270E mode — automatically switching to plain TN3270 mode"); ") in TN3270E mode — automatically switching to plain TN3270 mode");
tn3270eNegotiated = false; tn3270eNegotiated = false;
@@ -981,28 +1184,56 @@ public class TelnetFSM {
} }
} }
public void sendTN3270EPositiveResponse(int seqNumber) { public void processSCSInbound(byte[] data) {
if (data == null) return;
processSCSInbound(data, 0, data.length);
}
public void processSCSInbound(byte[] data, int offset, int length) {
log.fine("Processing SCS inbound data (" + length + " bytes)");
for (SCSInboundListener l : scsListeners) {
try {
l.onSCSDataReceived(data, offset, length);
} catch (Exception e) {
log.log(Level.WARNING, "Error in SCS inbound listener", e);
}
}
}
/**
* Send 5-byte/6-byte TN3270E response packet (DT_RESPONSE = 0x02).
*/
public void sendTn3270eResponse(byte responseFlag, byte responseData, int seq) {
byte[] resp = new byte[EH_SIZE + 1]; byte[] resp = new byte[EH_SIZE + 1];
resp[0] = (byte) DT_RESPONSE; resp[0] = (byte) DT_RESPONSE;
resp[1] = 0; resp[1] = 0;
resp[2] = (byte) RSF_POSITIVE_RESPONSE; resp[2] = responseFlag;
resp[3] = (byte) ((seqNumber >> 8) & 0xFF); resp[3] = (byte) ((seq >> 8) & 0xFF);
resp[4] = (byte) (seqNumber & 0xFF); resp[4] = (byte) (seq & 0xFF);
resp[5] = (byte) POS_DEVICE_END; resp[5] = responseData;
sendRecord(resp); sendRecord(resp);
} }
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) { /**
byte[] resp = new byte[EH_SIZE + 1]; * HoD 5-byte send_response compatible signature (com.ibm.eNetwork.ECL.tn3270.Telnet3270E).
resp[0] = (byte) DT_RESPONSE; */
resp[1] = 0; public void send_response(short s, short s2, int n) {
resp[2] = (byte) RSF_NEGATIVE_RESPONSE; byte[] byArray = new byte[5];
resp[3] = (byte) ((seqNumber >> 8) & 0xFF); byArray[0] = (byte) DT_RESPONSE;
resp[4] = (byte) (seqNumber & 0xFF); byArray[1] = (byte) s;
resp[5] = (byte) (negCode & 0xFF); byArray[2] = (byte) s2;
byArray[3] = (byte) ((n >> 8) & 0xFF);
byArray[4] = (byte) (n & 0xFF);
sendRecord(byArray);
}
sendRecord(resp); public void sendTN3270EPositiveResponse(int seqNumber) {
sendTn3270eResponse((byte) RSF_POSITIVE_RESPONSE, (byte) POS_DEVICE_END, seqNumber);
}
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
sendTn3270eResponse((byte) RSF_NEGATIVE_RESPONSE, (byte) (negCode & 0xFF), seqNumber);
} }
public void sendTN3270ESnaSenseResponse(int seqNumber, int sense1, int sense2) { public void sendTN3270ESnaSenseResponse(int seqNumber, int sense1, int sense2) {
@@ -1021,7 +1252,11 @@ public class TelnetFSM {
// ========== Check if we should transition to 3270 mode ========== // ========== Check if we should transition to 3270 mode ==========
private void checkIn3270() { private void checkIn3270() {
if (connectionState != ConnectionState.TELNET_PENDING) return; if (connectionState != ConnectionState.TELNET_PENDING &&
connectionState != ConnectionState.CONNECTED_NVT &&
connectionState != ConnectionState.CONNECTED_NVT_CHAR) {
return;
}
// For TN3270E, we wait for TN3270E negotiation to complete // For TN3270E, we wait for TN3270E negotiation to complete
if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) { if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
@@ -1032,7 +1267,11 @@ public class TelnetFSM {
if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] && if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] &&
myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) { myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) {
log.info("Transitioning to plain TN3270 mode"); log.info("Transitioning to plain TN3270 mode");
changeState(ConnectionState.CONNECTED_3270); if (connectionState != ConnectionState.CONNECTED_3270) {
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_3270);
notifyScreenUpdate();
}
} }
} }
@@ -1130,6 +1369,32 @@ public class TelnetFSM {
} }
} }
/**
* Send NVT data record (with TN3270E header and EOR framing if in TN3270E mode, or raw bytes).
*/
public void sendNvtData(byte[] data) {
if (data == null || data.length == 0) return;
if (tn3270eNegotiated && (connectionState == ConnectionState.CONNECTED_E_NVT || tn3270eSubmode == TN3270ESubmode.E_NVT)) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
out.write(DT_NVT_DATA);
out.write(0);
out.write(0);
out.write((eXmitSeq >> 8) & 0xFF);
out.write(eXmitSeq & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
for (byte b : data) {
out.write(b & 0xFF);
}
sendRecord(out.toByteArray());
} else {
sendBytes(data);
if (config != null && config.isNvtLocalEcho()) {
nvtProcessor.processNVTData(data, 0, data.length);
}
}
}
private void sendBytes(byte[] data) { private void sendBytes(byte[] data) {
try { try {
connection.sendRaw(data); connection.sendRaw(data);
@@ -1170,6 +1435,7 @@ public class TelnetFSM {
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; } public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
public String getConnectedLu() { return connectedLu; } public String getConnectedLu() { return connectedLu; }
public String getConnectedType() { return connectedType; } public String getConnectedType() { return connectedType; }
public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; }
// ========== SNA BIND, UNBIND, BID, DFC Handlers (Phase 2) ========== // ========== SNA BIND, UNBIND, BID, DFC Handlers (Phase 2) ==========
@@ -1434,6 +1700,10 @@ public class TelnetFSM {
return tn3270eBound; return tn3270eBound;
} }
public void processSysReq() {
handleSysReq();
}
public void handleSysReq() { public void handleSysReq() {
if (tn3270eNegotiated) { if (tn3270eNegotiated) {
byte[] ao = new byte[] { (byte) IAC, (byte) AO }; byte[] ao = new byte[] { (byte) IAC, (byte) AO };
@@ -0,0 +1,361 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.graphics.GocaConstants;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.printer.PrintSCS3270;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive test suite verifying all Phase 2 functional updates:
* 3270 Data Stream parsing, Graphic Escape & APL mapping, Partitioning,
* Structured Fields (0x0F, 0x20, 0x24, 0x40), and Query Reply builders.
*/
public class DataStreamProcessorPhase2FullTest {
private EbcdicTranslator translator;
private ScreenBuffer screen;
private DataStreamProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
processor = new DataStreamProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
@Test
public void testGraphicEscapeOrderWithAPLTranslation() {
// Write record: CMD_EW (0x05), WCC (0xC3), SBA to 0 (0x11, 0x40, 0x40), ORDER_GE (0x08), 0xA2 (s -> )
// followed by ORDER_GE, 0x85 (e -> ), ORDER_GE, 0xCB (Cross -> ), ORDER_GE, 0xBA (Omega -> Ω)
byte[] record = new byte[] {
(byte) CMD_EW, (byte) 0xC3,
(byte) ORDER_SBA, 0x40, 0x40,
(byte) ORDER_GE, (byte) 0xA2, // Horizontal Line
(byte) ORDER_GE, (byte) 0x85, // Vertical Line
(byte) ORDER_GE, (byte) 0xCB, // Cross
(byte) ORDER_GE, (byte) 0xBA // Greek Omega
};
processor.processRecord(record);
ExtendedAttribute cell0 = screen.getCell(0);
assertEquals(CS_GE, cell0.cs);
assertEquals((byte) 0xA2, cell0.ec);
assertEquals('─', cell0.ucs4);
ExtendedAttribute cell1 = screen.getCell(1);
assertEquals(CS_GE, cell1.cs);
assertEquals((byte) 0x85, cell1.ec);
assertEquals('│', cell1.ucs4);
ExtendedAttribute cell2 = screen.getCell(2);
assertEquals(CS_GE, cell2.cs);
assertEquals((byte) 0xCB, cell2.ec);
assertEquals('┼', cell2.ucs4);
ExtendedAttribute cell3 = screen.getCell(3);
assertEquals(CS_GE, cell3.cs);
assertEquals((byte) 0xBA, cell3.ec);
assertEquals('Ω', cell3.ucs4);
}
@Test
public void testRepeatToAddressWithGraphicEscape() {
// Repeat to address 5 with ORDER_GE prefix + 0xA2 ('─')
byte[] record = new byte[] {
(byte) CMD_EW, (byte) 0xC3,
(byte) ORDER_SBA, 0x40, 0x40, // Addr 0
(byte) ORDER_RA, 0x40, (byte) 0x45, // Addr 5
(byte) ORDER_GE, (byte) 0xA2
};
processor.processRecord(record);
for (int i = 0; i < 5; i++) {
ExtendedAttribute cell = screen.getCell(i);
assertEquals(CS_GE, cell.cs, "Cell " + i + " should have CS_GE");
assertEquals('─', cell.ucs4, "Cell " + i + " should have '─'");
}
}
@Test
public void testOutbound3270DSNonZeroPartition() {
// First create partition 2 (32 rows x 80 cols)
byte[] createPart = new byte[] {
(byte) CMD_WSF,
0x00, 0x08,
(byte) SF_CREATE_PART, 0x02,
0x00, 80,
0x00, 32
};
processor.processRecord(createPart);
assertEquals(2, screen.getActivePartition());
// Now activate partition 0
screen.setActivePartition(0);
assertEquals(0, screen.getActivePartition());
// Send Outbound 3270DS targeting Partition 2 containing Write command to write 'H' at pos 0
byte[] outboundDs = new byte[] {
(byte) CMD_WSF,
0x00, 0x0A, // Field length = 10
(byte) SF_OUTBOUND_DS, 0x02, // Target PID = 2
(byte) CMD_W, 0x00, // Write command with null WCC
(byte) ORDER_SBA, 0x40, 0x40, // Pos 0
(byte) 0xC8 // EBCDIC 'H'
};
processor.processRecord(outboundDs);
// Verify active partition was switched to PID 2
assertEquals(2, screen.getActivePartition());
assertEquals('H', screen.getCell(0).ucs4);
}
@Test
public void testStructuredField0x0FSetWindow() {
// SF 0x0F with explicit viewport window (xmin=10, ymin=20, xmax=500, ymax=300)
byte[] setWindowSf = new byte[] {
(byte) CMD_WSF,
0x00, 0x0B, // Length = 11
(byte) SF_SET_WINDOW,
0x00, 10, // xMin = 10
0x00, 20, // yMin = 20
0x01, (byte) 0xF4, // xMax = 500
0x01, 0x2C // yMax = 300
};
processor.processRecord(setWindowSf);
// Test direct overload processSFSetWindow
processor.processSFSetWindow(setWindowSf);
}
@Test
public void testStructuredField0x0FDataUnitActivatesGraphicCursor() {
assertFalse(processor.getGocaDecoder().isGraphicsCursorActive());
byte[] objDataSf = new byte[] {
(byte) CMD_WSF,
0x00, 0x04,
(byte) SF_SET_WINDOW,
(byte) GocaConstants.SF_OBJDATA_SUB // 0x0F
};
processor.processRecord(objDataSf);
assertTrue(processor.getGocaDecoder().isGraphicsCursorActive());
}
@Test
public void testStructuredField0x20ObjectControl() {
// SF 0x20 (3270 Graphics / Object Control) with GOCA begin/end area orders
byte[] objControl = new byte[] {
(byte) CMD_WSF,
0x00, 0x07,
(byte) SF_OBJECT_CONTROL,
(byte) GocaConstants.G_GBAR, 0x00, // Begin area
(byte) GocaConstants.G_GEAR, 0x00 // End area
};
processor.processRecord(objControl);
processor.processSFObjectControl(objControl);
}
@Test
public void testStructuredField0x24DocumentDataWithEmbeddedSCS() {
AtomicReference<byte[]> scsCaptured = new AtomicReference<>();
PrintSCS3270 mockScs = new PrintSCS3270(null, null, translator) {
@Override
public void processHostData(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
scsCaptured.set(b);
}
};
processor.setEmbeddedScsProcessor(mockScs);
// SF 0x24 (Document Data) with 4 bytes of SCS printer data
byte[] docData = new byte[] {
(byte) CMD_WSF,
0x00, 0x07,
(byte) SF_DOCUMENT_DATA,
(byte) 0x15, (byte) 0xC8, (byte) 0xC9, (byte) 0x15 // NL, 'H', 'I', NL
};
processor.processRecord(docData);
assertNotNull(scsCaptured.get());
assertEquals(4, scsCaptured.get().length);
assertEquals(0x15, scsCaptured.get()[0]);
assertEquals((byte) 0xC8, scsCaptured.get()[1]);
// Test direct method
processor.processSFDocumentData(docData);
}
@Test
public void testQueryReplyBuilderGraphicColor109Bytes() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] grColor = qrBuilder.buildGraphicColor();
assertNotNull(grColor);
assertEquals(105, grColor.length); // 105 bytes payload + 4 bytes header = 109 bytes in complete SF
// Check header bytes in payload: 0x00, 0x04, 0x00, 0xFF, 0xFF, 0x00, 0x10, 0x00, 0x10
assertEquals(0x00, grColor[0]);
assertEquals(0x04, grColor[1]);
assertEquals(0x00, grColor[2]);
assertEquals((byte) 0xFF, grColor[3]);
assertEquals((byte) 0xFF, grColor[4]);
assertEquals(0x00, grColor[5]);
assertEquals(0x10, grColor[6]);
assertEquals(0x00, grColor[7]);
assertEquals(0x10, grColor[8]);
// Check aliases
assertArrayEquals(grColor, qrBuilder.buildGrColor());
assertArrayEquals(grColor, qrBuilder.buildGColor());
}
@Test
public void testQueryReplyBuilderAuxDeviceAndLineType() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] lineType = qrBuilder.buildAuxDevice();
assertNotNull(lineType);
assertEquals(20, lineType.length); // 20 bytes payload + 4 bytes header = 24 bytes
assertArrayEquals(lineType, qrBuilder.buildLineType());
assertArrayEquals(lineType, qrBuilder.buildAuxDev());
}
@Test
public void testQueryReplyBuilderOemFormatAndPort() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] port = qrBuilder.buildOemFormat();
assertNotNull(port);
assertTrue(port.length > 0);
assertArrayEquals(port, qrBuilder.buildPort());
}
@Test
public void testQueryReplyBuilderUsableAreaAspectRatiosAndNoArgOverloads() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
// 1. Vector Graphics enabled (3179G standard: SDH = 16, flags = 0x03)
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
byte[] uaVector = qrBuilder.buildUsableArea();
assertNotNull(uaVector);
assertEquals(19, uaVector.length);
assertEquals(0x03, uaVector[0] & 0xFF); // Flags: 12/14 bit + Graphics
assertEquals(9, uaVector[15] & 0xFF); // AW = 9
assertEquals(16, uaVector[16] & 0xFF); // AH = 16
// 2. Text mode (GraphicsMode.NONE: SDH = 12, flags = 0x01)
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
byte[] uaText = qrBuilder.buildUsableArea();
assertNotNull(uaText);
assertEquals(0x01, uaText[0] & 0xFF); // Flags: 12/14 bit only
assertEquals(9, uaText[15] & 0xFF); // AW = 9
assertEquals(12, uaText[16] & 0xFF); // AH = 12
// 3. No-arg overloads check
assertNotNull(qrBuilder.buildSummary());
assertNotNull(qrBuilder.buildAlphaPartitions());
assertNotNull(qrBuilder.buildCharsets());
assertNotNull(qrBuilder.buildColor());
assertNotNull(qrBuilder.buildHighlighting());
assertNotNull(qrBuilder.buildReplyModes());
assertNotNull(qrBuilder.buildDdm());
assertNotNull(qrBuilder.buildImplicitPartition());
assertNotNull(qrBuilder.buildSegment());
assertNotNull(qrBuilder.buildProcedure());
assertNotNull(qrBuilder.buildGraphics());
assertNotNull(qrBuilder.buildGImage());
}
@Test
public void testQueryReplyBuilderColorTable18Bytes() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] colorTable = qrBuilder.buildColor();
assertNotNull(colorTable);
assertEquals(18, colorTable.length, "Color table payload must be exactly 18 bytes matching HoD DS3270.java:1850");
assertEquals(0x00, colorTable[0]);
assertEquals(0x08, colorTable[1]);
assertEquals(0x00, colorTable[2]);
assertEquals((byte) 0xF4, colorTable[3]); // Default green
}
@Test
public void testDataStreamProcessorConvenienceOverloads() {
// Test processEraseAllUnprotected
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(1).ucs4 = 'X';
screen.getCell(1).ec = (byte) 0xE7;
processor.processEraseAllUnprotected();
assertEquals(0, screen.getCell(1).ucs4);
// Test processReadModified and processReadModifiedAll
output.reset();
processor.processReadModified();
assertTrue(output.size() >= 3);
output.reset();
processor.processReadModifiedAll();
assertTrue(output.size() >= 3);
// Test processReadBuffer
output.reset();
processor.processReadBuffer();
assertTrue(output.size() >= 3);
// Test processEraseWrite and processEraseWriteAlternate
byte[] ewData = new byte[] { (byte) CMD_EW, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC1 };
processor.processEraseWrite(ewData);
assertEquals('A', screen.getCell(0).ucs4);
byte[] ewaData = new byte[] { (byte) CMD_EWA, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC2 };
processor.processEraseWriteAlternate(ewaData);
assertEquals('B', screen.getCell(0).ucs4);
// Test processSetReplyMode, processCreatePartition, processEraseReset byte[] overloads
byte[] srm = new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_SET_REPLY_MODE, 0x00, (byte) SF_SRM_CHAR };
processor.processSetReplyMode(srm);
assertEquals((byte) SF_SRM_CHAR, screen.getReplyMode());
byte[] cp = new byte[] { (byte) CMD_WSF, 0x00, 0x08, (byte) SF_CREATE_PART, 0x01, 0x00, 80, 0x00, 24 };
processor.processCreatePartition(cp);
assertEquals(1, screen.getActivePartition());
byte[] er = new byte[] { (byte) CMD_WSF, 0x00, 0x04, (byte) SF_ERASE_RESET, (byte) SF_ER_DEFAULT };
processor.processEraseReset(er);
assertEquals(0, screen.getActivePartition());
// Test query partition overloads
output.reset();
processor.processSFReadPartitionQuery(new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QUERY });
assertTrue(output.size() > 0);
output.reset();
processor.processSFReadPartitionQueryList(new byte[] {
(byte) CMD_WSF, 0x00, 0x07, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QLIST, (byte) SF_RPQ_LIST, (byte) QR_COLOR
});
assertTrue(output.size() > 0);
}
}
@@ -0,0 +1,128 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLOIAPhase3Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLOIA oia;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, input, null);
}
@Test
public void testAlphanumericTypeDetection() {
screen.erase(false);
// Unformatted screen
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
assertEquals("A", oia.getAlphanumericTypeString());
// Numeric field
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_NUMERIC));
screen.setCursorAddress(1);
assertEquals(TYPE_NUMERIC, oia.getAlphanumericType());
assertEquals("N", oia.getAlphanumericTypeString());
// Alphanumeric unprotected field
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
screen.setCursorAddress(11);
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
assertEquals("A", oia.getAlphanumericTypeString());
// DBCS cell
ExtendedAttribute ea = screen.getCell(11);
ea.cs = ExtendedAttribute.CS_DBCS;
assertEquals(TYPE_DBCS, oia.getAlphanumericType());
assertEquals("D", oia.getAlphanumericTypeString());
}
@Test
public void testOIAStatusFlagsAndStrings() {
screen.erase(false);
assertEquals("READY", oia.getStatusString());
assertFalse(oia.isXSystem());
assertFalse(oia.isXProt());
assertFalse(oia.isXNum());
assertFalse(oia.isXComm());
assertFalse(oia.isXOverflow());
assertFalse(oia.isXOperatorDue());
// Keyboard lock -> X-SYSTEM
input.setKeyboardLocked(true);
assertTrue(oia.isXSystem());
assertTrue(oia.isXWait());
assertEquals("X-SYSTEM", oia.getStatusString());
input.setKeyboardLocked(false);
// Protected field inhibit
oia.setInputInhibited(INHIBIT_PROTECTED_FIELD);
assertTrue(oia.isXProt());
assertEquals("X-PROT", oia.getStatusString());
// Numeric only inhibit
oia.setInputInhibited(INHIBIT_NUMERIC_ONLY);
assertTrue(oia.isXNum());
assertEquals("X-NUM", oia.getStatusString());
// Overflow inhibit
oia.setInputInhibited(INHIBIT_OVERFLOW);
assertTrue(oia.isXOverflow());
assertEquals("X-OVERFLOW", oia.getStatusString());
// Comm check inhibit
oia.setInputInhibited(INHIBIT_COMM_CHECK);
assertTrue(oia.isXComm());
assertEquals("X-COMM", oia.getStatusString());
// Operator due inhibit
oia.setInputInhibited(INHIBIT_OPERATOR_DUE);
assertTrue(oia.isXOperatorDue());
assertEquals("X-OP", oia.getStatusString());
// Insert mode
oia.setInputInhibited(INHIBIT_NOT_INHIBITED);
input.setInsertMode(true);
assertTrue(oia.isXInsert());
assertEquals("X-INSERT", oia.getStatusString());
}
@Test
public void testECLFieldOperations() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setChar(0, 1, 'X');
screen.setChar(0, 2, 'Y');
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLPS ps = new ECLPS(screen, input, translator);
ECLField f = ps.getFieldList().getFirstField();
assertNotNull(f);
assertEquals("XY ", f.getText());
assertFalse(f.isModified());
f.setModified(true);
assertTrue(f.isModified());
f.erase();
assertEquals(" ", f.getText());
assertFalse(f.isModified());
}
}
@@ -0,0 +1,124 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLPSPhase3Test implements ECLConstants {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testSearchPSOverloadsAndCaseOptions() {
screen.erase(false);
screen.setText("Host On-Demand ECL Automation");
// 1-based SearchPS
assertEquals(6, ps.SearchPS("On-Demand"));
assertEquals(6, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, true));
assertEquals(0, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, false));
assertEquals(0, ps.SearchPS("NONEXISTENT"));
// SearchPSExt
int pos = ps.SearchPSExt("ECL", 1, 50, SEARCH_FORWARD, false, false);
assertEquals(16, pos);
}
@Test
public void testSearchStringBackwardsAndWrap() {
screen.erase(false);
screen.setChar(0, 10, 'A');
screen.setChar(0, 11, 'B');
screen.setChar(0, 12, 'C');
screen.setChar(1, 20, 'A');
screen.setChar(1, 21, 'B');
screen.setChar(1, 22, 'C');
// Backward search starting from row 1 col 0 (addr 80) should find "ABC" at row 0 col 10 (addr 10)
int found = ps.searchString("ABC", 1, 0, SEARCH_BACKWARD, false);
assertEquals(10, found);
// Backward search starting from row 0 col 5 (addr 5) wraps around and finds "ABC" at row 1 col 20 (addr 100)
int wrapped = ps.searchString("ABC", 0, 5, SEARCH_BACKWARD, false);
assertEquals(100, wrapped);
}
@Test
public void testCopyStringRectangularExtraction() {
screen.erase(false);
// Row 0: "0123456789"
for (int c = 0; c < 10; c++) {
screen.setChar(0, c, (char) ('0' + c));
}
// Row 1: "ABCDEFGHIJ"
for (int c = 0; c < 10; c++) {
screen.setChar(1, c, (char) ('A' + c));
}
// Copy columns 2..5 of rows 0..1
String block = ps.copyString(0, 2, 1, 5);
assertEquals("2345\nCDEF", block);
}
@Test
public void testPasteStringRectangular() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
int pasted = ps.pasteString("HELLO\nWORLD", 0, 1);
assertEquals(10, pasted);
assertEquals("HELLO", ps.getString(1, 5));
assertEquals("WORLD", ps.getString(81, 5));
}
@Test
public void testSendCharactersWithDelay() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
ps.sendCharacters("ABC[tab]DEF", 2);
assertEquals('A', screen.getChar(0, 1));
assertEquals('B', screen.getChar(0, 2));
assertEquals('C', screen.getChar(0, 3));
assertEquals('D', screen.getChar(0, 11));
assertEquals('E', screen.getChar(0, 12));
assertEquals('F', screen.getChar(0, 13));
}
@Test
public void testWaitForScreenAndCursor() {
screen.erase(false);
assertFalse(ps.waitForScreen("READY", 50));
assertFalse(ps.waitForCursor(5, 10, 50));
screen.setText("READY");
assertTrue(ps.waitForScreen("READY", 50));
assertTrue(ps.waitForScreen("READY", 0, 0, 50));
assertFalse(ps.waitForScreen("READY", 1, 0, 50));
ps.setCursorPos(5, 10);
assertTrue(ps.waitForCursor(5, 10, 50));
}
}
@@ -114,4 +114,14 @@ public class ECLPSTest implements ECLConstants {
int count = ps.pasteLineWrap("ABC\nDEF", 1, 80, false); int count = ps.pasteLineWrap("ABC\nDEF", 1, 80, false);
assertEquals(6, count); assertEquals(6, count);
} }
@Test
public void testNVTModeAndSendKeys() {
assertFalse(ps.isNVTmode());
ps.setNVTmode(true);
assertTrue(ps.isNVTmode());
ps.setNVTmode(false);
assertFalse(ps.isNVTmode());
}
} }
@@ -288,6 +288,199 @@ public class GocaDecoderPhase5Test {
assertEquals(56, sf2.length); assertEquals(56, sf2.length);
assertEquals(0x00, sf2[0]); assertEquals(0x00, sf2[0]);
assertEquals(0x34, sf2[1]); assertEquals(0x34, sf2[1]);
byte[] sf3 = GraphicInputBuilder.buildPickCorrelation(120, 240, 5);
assertEquals(56, sf3.length);
assertEquals(120, ((sf3[24] & 0xFF) << 8) | (sf3[25] & 0xFF));
assertEquals(240, ((sf3[26] & 0xFF) << 8) | (sf3[27] & 0xFF));
}
@Test
public void testViewingWindowClipping() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Define viewing window covering center of presentation space: [-100..100, -100..100]
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSVW_DEF);
out.write(0x08);
out.write((byte) 0xFF); out.write((byte) 0x9C); // xMin = -100
out.write((byte) 0xFF); out.write((byte) 0x9C); // yMin = -100
out.write(0x00); out.write(100); // xMax = 100
out.write(0x00); out.write(100); // yMax = 100
// Draw a line inside presentation space but outside the viewing window: (-300, -150) -> (-200, -150)
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
out.write(GocaConstants.G_GLINE); out.write(0x08);
out.write((byte) 0xFE); out.write((byte) 0xD4); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-300, -150)
out.write((byte) 0xFF); out.write((byte) 0x38); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-200, -150)
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
// Pixels outside viewing window should have been clipped out
int pxClipped = plane.mapX(-300);
int pyClipped = plane.mapY(-150);
int pixelOutside = plane.getRgbBuffer()[pyClipped * plane.getCanvasWidth() + pxClipped];
assertEquals(0, pixelOutside, "Pixel outside viewing window must be 0 (clipped)");
// Now draw a line crossing the center (0, 0) -> (50, 50)
ByteArrayOutputStream insideStream = new ByteArrayOutputStream();
insideStream.write(GocaConstants.G_GLINE); insideStream.write(0x08);
insideStream.write(0x00); insideStream.write(0); insideStream.write(0x00); insideStream.write(0);
insideStream.write(0x00); insideStream.write(50); insideStream.write(0x00); insideStream.write(50);
byte[] inBytes = insideStream.toByteArray();
decoder.decodeGoca(inBytes, 0, inBytes.length);
int pxInside = plane.mapX(0);
int pyInside = plane.mapY(0);
int pixelInside = plane.getRgbBuffer()[pyInside * plane.getCanvasWidth() + pxInside];
assertNotEquals(0, pixelInside, "Pixel inside viewing window must be drawn");
}
@Test
public void testFractionalLineWidth() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Fractional Line Width: 2.5 (0x02 0x80)
out.write(GocaConstants.G_GSFLW);
out.write(0x02);
out.write(0x80);
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
assertEquals(2.5, decoder.getFractionalLineWidth(), 0.01);
assertEquals(2.5, plane.getFractionalLineWidth(), 0.01);
}
@Test
public void testSegmentCharacteristics() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Flags = 0xC0: Chained (0x80), Dynamic (0x40), Visible (0x00)
decoder.processSegmentCharacteristics(new byte[]{(byte) 0xC0});
assertTrue(decoder.isSegChained());
assertTrue(decoder.isSegDynamic());
assertTrue(decoder.isSegVisible());
// Flags = 0x20: Invisible
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x20});
assertFalse(decoder.isSegChained());
assertFalse(decoder.isSegDynamic());
assertFalse(decoder.isSegVisible());
}
@Test
public void testCharacterAngleAndShear() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Character Angle: 45 degrees vector (ax=10, ay=10)
out.write(GocaConstants.G_GSCA);
out.write(0x04);
out.write(0x00); out.write(10);
out.write(0x00); out.write(10);
// Set Character Shear: vector (sx=10, sy=0)
out.write(GocaConstants.G_GSCR);
out.write(0x04);
out.write(0x00); out.write(10);
out.write(0x00); out.write(0);
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
assertEquals(45.0, decoder.getCharAngle(), 0.01);
assertEquals(90.0, decoder.getCharShear(), 0.01);
}
@Test
public void testWindingRuleAreaFill() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
FillArea fillArea = new FillArea();
fillArea.setFillRule(GocaConstants.FILL_RULE_WINDING);
assertEquals(GocaConstants.FILL_RULE_WINDING, fillArea.getFillRule());
// Fill self-intersecting bow-tie polygon
int[] px = new int[]{50, 150, 50, 150};
int[] py = new int[]{50, 150, 150, 50};
fillArea.fill(plane, px, py, 4, 0xFF00FF00, GocaConstants.PT_SOLID, false, 0, 0, 0, 2, 0);
assertTrue(plane.hasContent());
}
@Test
public void testMultiBitAndCompressedImages() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
// Test 2-bit per pixel image
byte[] twoBppData = new byte[]{(byte) 0b00011011}; // 4 pixels: 0, 1 (Blue), 2 (Red), 3 (Green)
plane.drawImage(10, 10, 4, 1, twoBppData, 0xFFFFFFFF, GocaConstants.BPP_2, GocaConstants.IMG_UNCOMPRESSED);
assertTrue(plane.hasContent());
// Test 4-bit per pixel image
byte[] fourBppData = new byte[]{(byte) 0x12, (byte) 0x34}; // 4 pixels: 1, 2, 3, 4
plane.clear();
plane.drawImage(10, 10, 4, 1, fourBppData, 0xFFFFFFFF, GocaConstants.BPP_4, GocaConstants.IMG_UNCOMPRESSED);
assertTrue(plane.hasContent());
// Test RLE decompressed image
byte[] rleData = new byte[]{
0x04, (byte) 0xFF // 4 repeats of 0xFF
};
byte[] decompressed = GraphicsPlane.decompressGocaRle(rleData, 32, 1, GocaConstants.BPP_1);
assertEquals(4, decompressed.length);
assertEquals((byte) 0xFF, decompressed[0]);
assertEquals((byte) 0xFF, decompressed[3]);
}
@Test
public void testMultiColorProgrammedSymbolsOverlay() {
ProgramSymbolManager psm = new ProgramSymbolManager(9, 16);
// Load Red plane (colorPlane = 1) into Slot 4 (LCID 0x41, Start 0x40, RWS 0x04)
byte[] loadRed = new byte[4 + 6 + 18];
loadRed[0] = (byte) 0x81; // Extended header present (0x80) | Format 1
loadRed[1] = 0x41; // LCID
loadRed[2] = 0x40; // Start codepoint
loadRed[3] = 0x04; // RWS Slot 4 (Triple Plane)
loadRed[4] = 0x06; // Ext header length = 6
loadRed[5] = 0x00;
loadRed[6] = 9; // Cell width = 9
loadRed[7] = 16; // Cell height = 16
loadRed[8] = 0x00;
loadRed[9] = 0x01; // Plane = Red (1)
for (int i = 0; i < 18; i++) loadRed[10 + i] = (byte) 0xFF;
psm.loadps(loadRed);
// Load Green plane (colorPlane = 2) into the same slot without clearing (clearAll = false)
byte[] loadGreen = new byte[4 + 6 + 18];
loadGreen[0] = (byte) 0x81;
loadGreen[1] = 0x41;
loadGreen[2] = 0x40;
loadGreen[3] = 0x04;
loadGreen[4] = 0x06;
loadGreen[5] = 0x00;
loadGreen[6] = 9;
loadGreen[7] = 16;
loadGreen[8] = 0x00;
loadGreen[9] = 0x02; // Plane = Green (2)
for (int i = 0; i < 18; i++) loadGreen[10 + i] = (byte) 0xFF;
psm.loadps(loadGreen);
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x41, 0x40);
assertNotNull(slot);
byte[] pixels = slot.getPixelData();
assertNotNull(pixels);
// Pixels should be composite of Red (1) | Green (2) = Yellow (3)
assertEquals(3, pixels[0] & 0xFF);
} }
private static byte outCoord(int val) { private static byte outCoord(int val) {
@@ -0,0 +1,323 @@
package haus.nightmare.lib3270j.input;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class InputProcessorPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private ECLOIA oia;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
inputProcessor = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, inputProcessor, null);
inputProcessor.setOIA(oia);
}
@Test
public void testNumericOnlyValidation() {
// Setup numeric field at pos 0 (FA), data at 1..9
screen.setFieldAttribute(0, (byte) FA_NUMERIC);
screen.setFieldAttribute(10, (byte) FA_PROTECT); // terminate field
screen.setCursorAddress(1);
// Typing valid digits and punctuation
inputProcessor.typeCharacter('1');
assertEquals(2, screen.getCursorAddress());
assertEquals('1', (char) screen.getCell(1).ucs4);
assertFalse(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
inputProcessor.typeCharacter('-');
assertEquals(3, screen.getCursorAddress());
assertEquals('-', (char) screen.getCell(2).ucs4);
inputProcessor.typeCharacter('.');
assertEquals(4, screen.getCursorAddress());
assertEquals('.', (char) screen.getCell(3).ucs4);
inputProcessor.typeCharacter(' ');
assertEquals(5, screen.getCursorAddress());
// Typing invalid non-numeric character 'A' in numeric field
inputProcessor.typeCharacter('A');
assertTrue(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.getInputInhibited());
assertEquals("X-NUM", oia.getStatusString());
// Buffer position 5 should NOT have changed
assertEquals(5, screen.getCursorAddress());
assertEquals(0, screen.getCell(5).ec);
// Reset clears error and unlocks keyboard
inputProcessor.reset();
assertFalse(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
}
@Test
public void testProtectedFieldInhibition() {
// Setup protected field at pos 0, length 10
screen.setFieldAttribute(0, (byte) FA_PROTECT);
screen.setCursorAddress(1);
// Attempting to type into protected field
inputProcessor.typeCharacter('Z');
assertTrue(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_PROTECTED_FIELD, oia.getInputInhibited());
assertEquals("X-PROT", oia.getStatusString());
assertEquals(0, screen.getCell(1).ec);
// Reset unlocks
inputProcessor.reset();
assertFalse(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
}
@Test
public void testAutoSkipFieldBoundary() {
// Field 1: Unprotected at pos 0 (chars at 1, 2)
screen.setFieldAttribute(0, (byte) 0x00);
// Field 2: Auto-skip at pos 3 (FA_PROTECT | FA_NUMERIC) (chars at 4, 5)
screen.setFieldAttribute(3, (byte) (FA_PROTECT | FA_NUMERIC));
// Field 3: Unprotected at pos 6 (chars at 7, 8)
screen.setFieldAttribute(6, (byte) 0x00);
// Delimiter at pos 9
screen.setFieldAttribute(9, (byte) FA_PROTECT);
// Cursor at pos 1, type 'A' -> moves to pos 2
screen.setCursorAddress(1);
inputProcessor.typeCharacter('A');
assertEquals(2, screen.getCursorAddress());
// Type 'B' at pos 2 (the last char in Field 1) -> auto-skips over Field 2 (pos 3..5) to pos 7!
inputProcessor.typeCharacter('B');
assertEquals(7, screen.getCursorAddress());
}
@Test
public void testInsertModeFieldOverflowInhibition() {
// Unprotected field at pos 0, size 3 (positions 1, 2, 3)
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(4, (byte) FA_PROTECT);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('A');
inputProcessor.typeCharacter('B');
inputProcessor.typeCharacter('C');
// Field is full: pos 1='A', pos 2='B', pos 3='C'
assertEquals('A', (char) screen.getCell(1).ucs4);
assertEquals('B', (char) screen.getCell(2).ucs4);
assertEquals('C', (char) screen.getCell(3).ucs4);
// Enable insert mode and attempt to type at pos 1
inputProcessor.setInsertMode(true);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('X');
assertTrue(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_OVERFLOW, oia.getInputInhibited());
assertEquals("X-OVERFLOW", oia.getStatusString());
assertEquals('A', (char) screen.getCell(1).ucs4); // unchanged
}
@Test
public void testReadModifiedInboundDataFraming() {
// Create field at pos 0, type "TEST"
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(10, (byte) FA_PROTECT);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('T');
inputProcessor.typeCharacter('E');
inputProcessor.typeCharacter('S');
inputProcessor.typeCharacter('T');
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_ENTER, false);
assertNotNull(inbound);
assertTrue(inbound.length >= 7);
assertEquals((byte) AID_ENTER, inbound[0]); // AID
// Next 2 bytes: cursor address
// Next byte: SBA (0x11)
assertEquals((byte) ORDER_SBA, inbound[3]);
// Followed by field address (pos 1), then EBCDIC bytes for "TEST"
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[6]);
assertEquals((byte) translator.unicodeToEbcdic('E'), inbound[7]);
assertEquals((byte) translator.unicodeToEbcdic('S'), inbound[8]);
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[9]);
}
@Test
public void testAidSelectDoesNotTransmitFieldCharacters() {
// Create selectable field with designator ' ' at pos 1
screen.setFieldAttribute(0, (byte) (FA_INT_HIGH_SEL | FA_NUMERIC)); // selectable
screen.getCell(0).fa |= FA_MODIFY;
screen.getCell(1).ec = 0x40; // space designator
screen.getCell(2).ec = (byte) translator.unicodeToEbcdic('X');
screen.setFieldAttribute(10, (byte) FA_PROTECT);
screen.setCursorAddress(1);
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_SELECT, false);
assertNotNull(inbound);
assertEquals((byte) AID_SELECT, inbound[0]);
// Must contain SBA and designator address, but NO field text bytes
assertEquals(6, inbound.length); // AID (1) + Cursor (2) + SBA (1) + Addr (2) = 6 bytes
assertEquals((byte) ORDER_SBA, inbound[3]);
}
@Test
public void testPAKeysAndClearInboundData() {
screen.setCursorAddress(80);
byte[] pa1 = inputProcessor.buildReadModifiedInboundData(AID_PA1, false);
assertEquals(3, pa1.length); // AID + 2 cursor bytes
assertEquals((byte) AID_PA1, pa1[0]);
byte[] clear = inputProcessor.buildReadModifiedInboundData(AID_CLEAR, false);
assertEquals(3, clear.length);
assertEquals((byte) AID_CLEAR, clear[0]);
}
@Test
public void testReadBufferInboundDataStandardAndExtended() {
screen.setFieldAttribute(0, (byte) 0x00);
screen.getCell(1).ec = (byte) translator.unicodeToEbcdic('A');
screen.setFieldAttribute(10, (byte) FA_PROTECT);
// Standard Field Mode
screen.setReplyMode((byte) SF_SRM_FIELD);
byte[] standardBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
assertNotNull(standardBuf);
assertEquals((byte) AID_ENTER, standardBuf[0]);
assertEquals((byte) ORDER_SF, standardBuf[3]);
// Extended Field Mode
screen.setReplyMode((byte) SF_SRM_XFIELD);
byte[] extendedBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
assertNotNull(extendedBuf);
assertEquals((byte) AID_ENTER, extendedBuf[0]);
assertEquals((byte) ORDER_SFE, extendedBuf[3]);
}
@Test
public void testWordLeftWordRightAndFieldEndNavigation() {
// Field: "HELLO WORLD "
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(25, (byte) FA_PROTECT);
String text = "HELLO WORLD ";
for (int i = 0; i < text.length(); i++) {
screen.getCell(1 + i).ec = (byte) translator.unicodeToEbcdic(text.charAt(i));
screen.getCell(1 + i).ucs4 = text.charAt(i);
}
// Test Field End
screen.setCursorAddress(1);
inputProcessor.processFieldEnd();
// Immediately following 'D' in WORLD (pos 1 + 13 = 14)
assertEquals(14, screen.getCursorAddress());
// Test Word Left from pos 14 -> jumps to start of "WORLD" (pos 9)
inputProcessor.processWordLeft();
assertEquals(9, screen.getCursorAddress());
// Test Word Left from pos 9 -> jumps to start of "HELLO" (pos 1)
inputProcessor.processWordLeft();
assertEquals(1, screen.getCursorAddress());
// Test Word Right from pos 1 -> jumps to start of "WORLD" (pos 9)
inputProcessor.processWordRight();
assertEquals(9, screen.getCursorAddress());
}
@Test
public void testAll22CompatibleMethodsAndOverloads() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
Telnet3270Client client = new Telnet3270Client(config);
// 1. processChar
client.processChar('A');
client.processChar('B', 5, false);
// 2. processEnter
client.processEnter();
// 3. processPF
client.processPF(3);
// 4. processPA
client.processPA(1);
// 5. processClear
client.processClear();
// 6-9. Cursor navigation
client.processCursorUp();
client.processCursorDown();
client.processCursorLeft();
client.processCursorRight();
// 10-13. Field navigation
client.processTab();
client.processBackTab();
client.processHome();
client.processNewline();
// 14-16. Editing
client.processDelete();
client.processBackspace();
client.processEraseEOF();
client.processEraseInput();
// 17-19. Special orders & toggles
client.processDup();
client.processFieldMark();
client.processToggleInsert();
// 20-22. Control / Selection
client.processReset();
client.processWordLeft();
client.processWordRight();
client.processFieldEnd();
client.processAttn();
client.processSysReq();
client.processCurSel();
client.processLightPen();
client.processLightPen(10);
assertNotNull(client.getInputProcessor().buildReadModifiedInboundData());
assertNotNull(client.getInputProcessor().buildReadBufferInboundData());
}
@Test
public void testSendKeysMnemonicTokens() {
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(15, (byte) 0x00);
screen.setFieldAttribute(30, (byte) FA_PROTECT);
screen.setCursorAddress(1);
inputProcessor.sendKeys("123[tab]456[wordleft][fieldend][reset]");
assertEquals('1', (char) screen.getCell(1).ucs4);
assertEquals('2', (char) screen.getCell(2).ucs4);
assertEquals('3', (char) screen.getCell(3).ucs4);
assertEquals('4', (char) screen.getCell(16).ucs4);
assertEquals('5', (char) screen.getCell(17).ucs4);
assertEquals('6', (char) screen.getCell(18).ucs4);
assertFalse(inputProcessor.isKeyboardLocked());
}
}
@@ -453,17 +453,15 @@ public class InputProcessorTest {
} }
@Test @Test
public void testSendAidWhenGraphicsCursorActiveFraming() { public void testSendAidAlwaysSendsStandard3270StreamEvenIfGraphicCursorActive() {
screen.erase(false); screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY)); screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1; // 'A' screen.getCell(1).ec = (byte) 0xC1; // 'A'
screen.setCellFA(5, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCursorAddress(2); screen.setCursorAddress(2);
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600); haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane); haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(true); goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(150, -80);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>(); java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) { InputProcessor input = new InputProcessor(screen, translator, null) {
@@ -478,64 +476,10 @@ public class InputProcessorTest {
byte[] result = sent.get(); byte[] result = sent.get();
assertNotNull(result); assertNotNull(result);
// Total expected length: // Standard 3270 stream: AID_ENTER (1) + Cursor Addr (2) + SBA (1) + Field Addr (2) + Data 'A' (1) = 7 bytes
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes assertEquals(7, result.length);
assertEquals(64, result.length); assertEquals((byte) AID_ENTER, result[0]);
assertEquals((byte) AID_SF, result[0]); assertEquals((byte) ORDER_SBA, result[3]);
// SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification assertEquals((byte) 0xC1, result[6]);
assertEquals(0x00, result[1]);
assertEquals(0x34, result[2]);
// SF ID = 0x0F0F
assertEquals(0x0F, result[3]);
assertEquals(0x0F, result[4]);
// Coordinates in SF at index 1 + 24 = 25
int gx = (result[25] << 8) | (result[26] & 0xFF);
int gy = (result[27] << 8) | (result[28] & 0xFF);
assertEquals(150, (short) gx);
assertEquals(-80, (short) gy);
// Keyboard constants at index 1 + 31 = 32 and 1 + 33 = 34
assertEquals(0x07, result[32]);
assertEquals(0x07, result[34]);
assertEquals((byte) 0xFF, result[35]);
assertEquals((byte) AID_ENTER, result[36]);
// Trailing AID at index 57
assertEquals((byte) AID_ENTER, result[57]);
// Trailing SBA at 60
assertEquals((byte) ORDER_SBA, result[60]);
// Trailing field content 'A' at 63
assertEquals((byte) 0xC1, result[63]);
}
@Test
public void testSendAidPAWhenGraphicsCursorActiveFraming() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1;
screen.setCursorAddress(2);
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(100, 200);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sent.set(data);
}
};
input.setGocaDecoder(goca);
input.sendAid(AID_PA1);
byte[] result = sent.get();
assertNotNull(result);
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_PA1 0x6C) + 2 (Cursor Addr) = 60 bytes (no modified field data)
assertEquals(60, result.length);
assertEquals((byte) AID_SF, result[0]);
assertEquals((byte) AID_PA1, result[36]); // Keyboard AID in SF
assertEquals((byte) AID_PA1, result[57]); // Trailing AID
} }
} }
@@ -138,4 +138,125 @@ public class NvtProcessorTest {
assertEquals('\r', (char) sent[5]); assertEquals('\r', (char) sent[5]);
assertEquals('\n', (char) sent[6]); assertEquals('\n', (char) sent[6]);
} }
@Test
public void testCursorPositionReportDSR() {
// Move to row 12, col 34 (0-indexed: row 11, col 33)
byte[] move = "\u001B[12;34H".getBytes();
processor.processNVTData(move, 0, move.length);
output.reset();
// Send DSR Cursor Position Request: ESC [ 6 n
byte[] dsr = "\u001B[6n".getBytes();
processor.processNVTData(dsr, 0, dsr.length);
assertEquals("\u001B[12;34R", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
}
@Test
public void testDeviceAttributesDA() {
output.reset();
// Send DA Request: ESC [ c
byte[] da1 = "\u001B[c".getBytes();
processor.processNVTData(da1, 0, da1.length);
assertEquals("\u001B[?1;2c", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
output.reset();
// Send DA Request: ESC [ 0 c
byte[] da2 = "\u001B[0c".getBytes();
processor.processNVTData(da2, 0, da2.length);
assertEquals("\u001B[?1;2c", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
}
@Test
public void testExtendedAnsiSequences() {
// CHA: Cursor Horizontal Absolute -> ESC [ 20 G (moves to col 20 -> 0-indexed col 19)
byte[] cha = "\u001B[20G".getBytes();
processor.processNVTData(cha, 0, cha.length);
assertEquals(19, screen.getCursorAddress());
// CNL: Cursor Next Line -> ESC [ 2 E (down 2 rows, col 0)
byte[] cnl = "\u001B[2E".getBytes();
processor.processNVTData(cnl, 0, cnl.length);
assertEquals(2 * 80, screen.getCursorAddress());
// CPL: Cursor Previous Line -> ESC [ 1 F (up 1 row, col 0)
byte[] cpl = "\u001B[1F".getBytes();
processor.processNVTData(cpl, 0, cpl.length);
assertEquals(1 * 80, screen.getCursorAddress());
// Write line "ABCDE"
byte[] text = "\u001B[1;1HABCDE".getBytes();
processor.processNVTData(text, 0, text.length);
assertEquals('A', screen.getCell(0).ucs4);
assertEquals('E', screen.getCell(4).ucs4);
// Move to pos 2 ('C') and ECH (Erase Character): ESC [ 2 X
byte[] ech = "\u001B[1;3H\u001B[2X".getBytes();
processor.processNVTData(ech, 0, ech.length);
assertEquals('A', screen.getCell(0).ucs4);
assertEquals('B', screen.getCell(1).ucs4);
assertEquals(' ', screen.getCell(2).ucs4);
assertEquals(' ', screen.getCell(3).ucs4);
assertEquals('E', screen.getCell(4).ucs4);
// DCH: Delete Character at pos 0 -> ESC [ 1;1H ESC [ 1 P
byte[] dch = "\u001B[1;1H\u001B[1P".getBytes();
processor.processNVTData(dch, 0, dch.length);
assertEquals('B', screen.getCell(0).ucs4);
// ICH: Insert Character at pos 0 -> ESC [ 1;1H ESC [ 1 @
byte[] ich = "\u001B[1;1H\u001B[1@".getBytes();
processor.processNVTData(ich, 0, ich.length);
assertEquals(' ', screen.getCell(0).ucs4);
assertEquals('B', screen.getCell(1).ucs4);
}
@Test
public void testScrollingRegionAndSpecialMovement() {
// Set scrolling region lines 2..4 (1-indexed): ESC [ 2 ; 4 r
byte[] decstbm = "\u001B[2;4r".getBytes();
processor.processNVTData(decstbm, 0, decstbm.length);
assertEquals(0, screen.getCursorAddress());
// Move to row 4 (bottom of region), col 1
byte[] line4 = "\u001B[4;1HLine4".getBytes();
processor.processNVTData(line4, 0, line4.length);
assertEquals('L', screen.getCell(3 * 80).ucs4);
// Index (line feed down at bottom of region): ESC D -> scrolls region up
byte[] ind = "\u001BD".getBytes();
processor.processNVTData(ind, 0, ind.length);
assertEquals('L', screen.getCell(2 * 80).ucs4); // Moved to row 3
// Reverse Index (line feed up at top of region): ESC [ 2;1H ESC M -> scrolls region down
byte[] ri = "\u001B[2;1H\u001BM".getBytes();
processor.processNVTData(ri, 0, ri.length);
assertEquals('L', screen.getCell(3 * 80).ucs4); // Moved back down to row 4
}
@Test
public void testCursorVisibilityAndVt100Graphics() {
assertTrue(processor.isCursorVisible());
// Hide cursor: ESC [ ? 25 l
byte[] hide = "\u001B[?25l".getBytes();
processor.processNVTData(hide, 0, hide.length);
assertFalse(processor.isCursorVisible());
// Show cursor: ESC [ ? 25 h
byte[] show = "\u001B[?25h".getBytes();
processor.processNVTData(show, 0, show.length);
assertTrue(processor.isCursorVisible());
// Enable VT100 line drawing on G0: ESC ( 0
byte[] g0 = "\u001B(0q".getBytes();
processor.processNVTData(g0, 0, g0.length);
assertEquals('─', screen.getCell(0).ucs4);
// Reset G0 to ASCII: ESC ( B
byte[] g0Ascii = "\u001B(Bq".getBytes();
processor.processNVTData(g0Ascii, 0, g0Ascii.length);
assertEquals('q', screen.getCell(1).ucs4);
}
} }
@@ -0,0 +1,278 @@
package haus.nightmare.lib3270j.screen;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.AbstractDBCSCodePage;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ScreenBufferPhase3Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
}
@Test
public void testWrappedFieldListFirstFaNotAtZero() {
screen.erase(false);
// Place FA at pos 100 (unprotected)
screen.setFieldAttribute(100, (byte) FA_PRINTABLE);
// Place FA at pos 150 (protected) - wraps around 1919 back to 99
screen.setFieldAttribute(150, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLFieldList list = screen.buildFieldList();
assertEquals(2, list.getFieldCount());
ECLField f1 = list.getFirstField();
assertNotNull(f1);
assertEquals(100, f1.getStart());
assertEquals(101, f1.getDataStart());
assertEquals(149, f1.getEnd());
assertEquals(49, f1.getLength());
assertFalse(f1.isWrapped());
assertFalse(f1.isProtected());
ECLField f2 = list.getNextField(f1);
assertNotNull(f2);
assertEquals(150, f2.getStart());
assertEquals(151, f2.getDataStart());
assertEquals(99, f2.getEnd());
assertEquals(1920 - 150 - 1 + 100, f2.getLength());
assertTrue(f2.isWrapped());
assertTrue(f2.isProtected());
// Test field containment on wrapped field
assertTrue(f2.contains(150));
assertTrue(f2.contains(151));
assertTrue(f2.contains(1919));
assertTrue(f2.contains(0));
assertTrue(f2.contains(99));
assertFalse(f2.contains(100));
assertFalse(f2.contains(120));
// Test field lookup at position
assertEquals(150, screen.findFieldAt(50).getStart());
assertEquals(100, screen.findFieldAt(120).getStart());
// Test navigation
assertEquals(150, screen.findPrevField(120).getStart());
assertEquals(150, screen.findNextField(120).getStart());
}
@Test
public void testSingleFieldScreenWrap() {
screen.erase(false);
screen.setFieldAttribute(50, (byte) FA_PRINTABLE);
ECLFieldList list = screen.buildFieldList();
assertEquals(1, list.getFieldCount());
ECLField f = list.getFirstField();
assertNotNull(f);
assertEquals(50, f.getStart());
assertEquals(51, f.getDataStart());
assertEquals(49, f.getEnd());
assertEquals(1919, f.getLength());
assertTrue(f.isWrapped());
assertTrue(f.contains(0));
assertTrue(f.contains(50));
assertTrue(f.contains(1919));
}
@Test
public void testInsertCharSBCSAndOverflow() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(6, (byte) (FA_PRINTABLE | FA_PROTECT)); // field of 5 chars (pos 1..5)
// Write 'A', 'B', 'C', 'D' at pos 1..4 (pos 5 is null)
screen.setChar(0, 1, 'A');
screen.setChar(0, 2, 'B');
screen.setChar(0, 3, 'C');
screen.setChar(0, 4, 'D');
// Insert 'Z' at pos 1
boolean inserted = screen.insertChar(1, 'Z');
assertTrue(inserted);
assertEquals('Z', screen.getChar(0, 1));
assertEquals('A', screen.getChar(0, 2));
assertEquals('B', screen.getChar(0, 3));
assertEquals('C', screen.getChar(0, 4));
assertEquals('D', screen.getChar(0, 5));
// Field is now full (pos 5 is 'D'). Inserting another char should overflow and return false
boolean overflow = screen.insertChar(1, 'X');
assertFalse(overflow);
}
@Test
public void testInsertAndShiftDBCSChar() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
// Insert DBCS char at pos 1
boolean ok = dbcsScreen.insertChar(1, '\u6771');
assertTrue(ok);
assertEquals(0x43, dbcsScreen.getCell(1).ec & 0xFF);
assertEquals(0x41, dbcsScreen.getCell(2).ec & 0xFF);
assertEquals(ExtendedAttribute.CS_DBCS, dbcsScreen.getCell(1).cs);
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
assertEquals(3, dbcsScreen.getCursorAddress());
}
@Test
public void testDeleteCharDBCSAndCleanAdjacentSISO() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771');
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
// Insert DBCS character at pos 1
dbcsScreen.insertChar(1, '\u6771');
// Delete DBCS character at pos 1
boolean deleted = dbcsScreen.deleteChar(1);
assertTrue(deleted);
assertEquals(0, dbcsScreen.getCell(1).ec);
assertEquals(0, dbcsScreen.getCell(2).ec);
// Test orphaned SO/SI cleanup on delete
dbcsScreen.setCell(3, 0x0E); // SO
dbcsScreen.setCell(4, 0x43); // DBCS byte 1
dbcsScreen.setCell(5, 0x41); // DBCS byte 2
dbcsScreen.setCell(6, 0x0F); // SI
dbcsScreen.getCell(4).cs = ExtendedAttribute.CS_DBCS;
dbcsScreen.getCell(4).db = ExtendedAttribute.DB_LEFT;
dbcsScreen.getCell(5).cs = ExtendedAttribute.CS_DBCS;
dbcsScreen.getCell(5).db = ExtendedAttribute.DB_RIGHT;
// Deleting the DBCS character at pos 4 pulls SI (pos 6) adjacent to SO (pos 3), triggering SISO cleanup
dbcsScreen.deleteChar(4);
assertEquals(0, dbcsScreen.getCell(3).ec);
assertEquals(0, dbcsScreen.getCell(4).ec);
assertEquals(0, dbcsScreen.getCell(5).ec);
assertEquals(0, dbcsScreen.getCell(6).ec);
}
@Test
public void testEntryAssistDOCModeAndTabStops() {
screen.erase(false);
assertFalse(screen.isEntryAssistDOCmode());
assertFalse(screen.isEntryAssistWordWrap());
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
screen.setEntryAssistStartColumn(5);
screen.setEntryAssistEndColumn(75);
screen.setEntryAssistTabStops(new int[]{ 10, 20, 30, 40 });
assertTrue(screen.isEntryAssistDOCmode());
assertTrue(screen.isEntryAssistWordWrap());
assertEquals(5, screen.getEntryAssistStartColumn());
assertEquals(75, screen.getEntryAssistEndColumn());
assertArrayEquals(new int[]{ 10, 20, 30, 40 }, screen.getEntryAssistTabStops());
// Test word tab forward with tab stops
screen.setCursorPosition(0, 0);
screen.processWordTab(true);
assertEquals(10, screen.getCursorCol());
screen.processWordTab(true);
assertEquals(20, screen.getCursorCol());
// Test word tab backward
screen.processWordTab(false);
assertEquals(10, screen.getCursorCol());
}
@Test
public void testProcessDeleteWord() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
// Write "HELLO WORLD" starting at pos 1
String text = "HELLO WORLD";
for (int i = 0; i < text.length(); i++) {
screen.setChar(0, 1 + i, text.charAt(i));
}
screen.setCursorAddress(1);
screen.processDeleteWord();
// "HELLO " is deleted, "WORLD" is shifted left to pos 1
assertEquals('W', screen.getChar(0, 1));
assertEquals('O', screen.getChar(0, 2));
assertEquals('R', screen.getChar(0, 3));
assertEquals('L', screen.getChar(0, 4));
assertEquals('D', screen.getChar(0, 5));
}
@Test
public void testProcessSOSIDisplayTransformation() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771');
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
// Put SO (0x0E), 0x43, 0x41, SI (0x0F) at pos 0..3
dbcsScreen.setCell(0, 0x0E);
dbcsScreen.setCell(1, 0x43);
dbcsScreen.setCell(2, 0x41);
dbcsScreen.setCell(3, 0x0F);
dbcsScreen.processSOSI();
assertEquals(ExtendedAttribute.DB_SO, dbcsScreen.getCell(0).db);
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
assertEquals(ExtendedAttribute.DB_SI, dbcsScreen.getCell(3).db);
assertEquals('\u6771', dbcsScreen.getCell(1).ucs4);
assertEquals('\u6771', dbcsScreen.getCell(2).ucs4);
}
@Test
public void testAccessorsAndConvenienceMethods() {
screen.erase(false);
assertEquals(24 * 80, screen.getSize());
screen.writeChar(10, (byte) 0xC1); // 'A' in CP037
assertEquals('A', screen.getChar(0, 10));
screen.setChar(1, 5, 'Z');
assertEquals('Z', screen.getChar(1, 5));
ExtendedAttribute ea = new ExtendedAttribute();
ea.fg = 2; // RED
screen.setExtAttr(1, 5, ea);
assertEquals(2, screen.getExtAttr(1, 5).fg);
screen.setText("TESTING 123");
assertEquals("TESTING 123", screen.getString(0, 11));
assertEquals(0, screen.searchString("TESTING"));
assertEquals(-1, screen.searchString("NOTFOUND"));
}
}
@@ -0,0 +1,244 @@
package haus.nightmare.lib3270j.telnet;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;
public class ProxyConnectionTest {
@Test
public void testParseHostStringWithProxyFlags() {
// HTTP proxy without auth
ConnectionConfig c1 = ConnectionConfig.parseHostString("--proxy=http://proxy.corp.com:8080 mainframe.net:23", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.HTTP, c1.getProxyType());
assertEquals("proxy.corp.com", c1.getProxyHost());
assertEquals(8080, c1.getProxyPort());
assertNull(c1.getProxyUsername());
assertEquals("mainframe.net", c1.getHost());
assertEquals(23, c1.getPort());
// HTTP proxy with auth
ConnectionConfig c2 = ConnectionConfig.parseHostString("--proxy=http://alice:secret123@10.0.0.1:3128 zos.ibm.com:2323", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.HTTP, c2.getProxyType());
assertEquals("10.0.0.1", c2.getProxyHost());
assertEquals(3128, c2.getProxyPort());
assertEquals("alice", c2.getProxyUsername());
assertEquals("secret123", c2.getProxyPassword());
assertEquals("zos.ibm.com", c2.getHost());
assertEquals(2323, c2.getPort());
// SOCKS4 proxy
ConnectionConfig c3 = ConnectionConfig.parseHostString("--proxy=socks4://socks.local:1080 L:secure.mvs.com:992", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.SOCKS4, c3.getProxyType());
assertEquals("socks.local", c3.getProxyHost());
assertEquals(1080, c3.getProxyPort());
assertTrue(c3.isUseTls());
assertEquals("secure.mvs.com", c3.getHost());
assertEquals(992, c3.getPort());
// SOCKS5 proxy with auth
ConnectionConfig c4 = ConnectionConfig.parseHostString("--proxy=socks5://bob:pass55@127.0.0.1:9050 P:vm.host:23", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.SOCKS5, c4.getProxyType());
assertEquals("127.0.0.1", c4.getProxyHost());
assertEquals(9050, c4.getProxyPort());
assertEquals("bob", c4.getProxyUsername());
assertEquals("pass55", c4.getProxyPassword());
assertFalse(c4.isTn3270eEnabled());
assertEquals("vm.host", c4.getHost());
}
@Test
public void testHttpProxyConnectHandshake() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch serverHandshakeDone = new CountDownLatch(1);
CompletableFuture<String> receivedRequest = new CompletableFuture<>();
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSock.getInputStream(), StandardCharsets.US_ASCII));
StringBuilder req = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) break;
req.append(line).append("\n");
}
receivedRequest.complete(req.toString());
// Respond 200 Connection established
OutputStream out = clientSock.getOutputStream();
out.write("HTTP/1.1 200 Connection established\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
out.flush();
serverHandshakeDone.countDown();
// Echo back test data
int b;
while ((b = clientSock.getInputStream().read()) != -1) {
out.write(b);
out.flush();
}
} catch (Exception e) {
receivedRequest.completeExceptionally(e);
}
});
serverThread.setDaemon(true);
serverThread.start();
ConnectionConfig config = new ConnectionConfig("target.mainframe.org", 23);
config.setProxy(ConnectionConfig.ProxyType.HTTP, "127.0.0.1", proxyPort, "testuser", "testpass");
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
String req = receivedRequest.get(3, TimeUnit.SECONDS);
assertTrue(req.startsWith("CONNECT target.mainframe.org:23 HTTP/1.1"), "Expected CONNECT request");
assertTrue(req.contains("Proxy-Authorization: Basic "), "Expected Basic auth in proxy request");
connection.disconnect();
}
}
@Test
public void testSocks4ProxyHandshake() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch handshakeDone = new CountDownLatch(1);
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
InputStream in = clientSock.getInputStream();
OutputStream out = clientSock.getOutputStream();
// Read SOCKS4 request header
byte[] req = new byte[8];
in.read(req);
assertEquals(0x04, req[0]); // SOCKS4
assertEquals(0x01, req[1]); // CONNECT
// Read null-terminated username
ByteArrayOutputStream userBuf = new ByteArrayOutputStream();
int b;
while ((b = in.read()) != 0 && b != -1) {
userBuf.write(b);
}
assertEquals("user4", new String(userBuf.toByteArray()));
// Reply 0x00 0x5A (Request granted)
byte[] resp = new byte[] { 0x00, 0x5A, 0x00, 0x17, 127, 0, 0, 1 };
out.write(resp);
out.flush();
handshakeDone.countDown();
while (in.read() != -1) {}
} catch (Exception ignored) {}
});
serverThread.setDaemon(true);
serverThread.start();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", 23);
config.setProxy(ConnectionConfig.ProxyType.SOCKS4, "127.0.0.1", proxyPort, "user4", null);
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
connection.disconnect();
}
}
@Test
public void testSocks5ProxyHandshakeWithAuth() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch handshakeDone = new CountDownLatch(1);
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
InputStream in = clientSock.getInputStream();
OutputStream out = clientSock.getOutputStream();
// 1. Read method selection
int ver = in.read();
int nmethods = in.read();
byte[] methods = new byte[nmethods];
in.read(methods);
assertEquals(0x05, ver);
// Select USER_PASS (0x02)
out.write(new byte[] { 0x05, 0x02 });
out.flush();
// 2. Read auth request (RFC 1929)
int authVer = in.read();
int ulen = in.read();
byte[] u = new byte[ulen];
in.read(u);
int plen = in.read();
byte[] p = new byte[plen];
in.read(p);
assertEquals(1, authVer);
assertEquals("admin", new String(u));
assertEquals("pass123", new String(p));
// Auth success: 0x01 0x00
out.write(new byte[] { 0x01, 0x00 });
out.flush();
// 3. Read connect command
byte[] cmd = new byte[4];
in.read(cmd);
assertEquals(0x05, cmd[0]);
assertEquals(0x01, cmd[1]); // CONNECT
int atyp = cmd[3];
if (atyp == 0x03) { // Domain name
int dlen = in.read();
byte[] d = new byte[dlen];
in.read(d);
} else if (atyp == 0x01) { // IPv4
in.read(new byte[4]);
}
in.read(new byte[2]); // Port
// Reply success: 0x05 0x00 0x00 0x01 127.0.0.1:port
out.write(new byte[] { 0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0, 23 });
out.flush();
handshakeDone.countDown();
while (in.read() != -1) {}
} catch (Exception ignored) {}
});
serverThread.setDaemon(true);
serverThread.start();
ConnectionConfig config = new ConnectionConfig("mvs.corp.local", 23);
config.setProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", proxyPort, "admin", "pass123");
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
connection.disconnect();
}
}
}
@@ -0,0 +1,346 @@
package haus.nightmare.lib3270j.telnet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.SCSInboundListener;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
public class TelnetFSMPhase1FullTest {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private InputProcessor inputProcessor;
private TelnetFSM fsm;
private MockConnection connection;
private EbcdicTranslator translator;
private static class MockConnection extends TelnetConnection {
final List<byte[]> sentData = new ArrayList<>();
boolean tlsUpgraded = false;
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
super(config, fsm);
}
@Override
public synchronized void sendRaw(byte[] data) {
sentData.add(data.clone());
}
@Override
public synchronized void sendRaw(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
sentData.add(b);
}
@Override
public synchronized void upgradeToTls() throws IOException {
tlsUpgraded = true;
}
}
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
dsProcessor = new DataStreamProcessor(screenBuffer, translator);
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
dsProcessor.setInputProcessor(inputProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
private void feedBytes(int... bytes) {
for (int b : bytes) {
fsm.feedByte(b & 0xFF);
}
}
@Test
public void testSendTn3270eResponseAndHoD5ByteSignature() {
connection.sentData.clear();
// 1. Standard 6-byte RFC 2355 response
fsm.sendTn3270eResponse((byte) RSF_POSITIVE_RESPONSE, (byte) POS_DEVICE_END, 0x1234);
assertEquals(1, connection.sentData.size());
byte[] resp6 = connection.sentData.get(0);
assertEquals(6 + 2, resp6.length); // 6 bytes header/trailer + IAC EOR
assertEquals(DT_RESPONSE, resp6[0] & 0xFF);
assertEquals(0, resp6[1] & 0xFF);
assertEquals(RSF_POSITIVE_RESPONSE, resp6[2] & 0xFF);
assertEquals(0x12, resp6[3] & 0xFF);
assertEquals(0x34, resp6[4] & 0xFF);
assertEquals(POS_DEVICE_END, resp6[5] & 0xFF);
assertEquals(IAC, resp6[6] & 0xFF);
assertEquals(EOR, resp6[7] & 0xFF);
// 2. HoD 5-byte send_response
connection.sentData.clear();
fsm.send_response((short) 0, (short) RSF_POSITIVE_RESPONSE, 0x5678);
assertEquals(1, connection.sentData.size());
byte[] resp5 = connection.sentData.get(0);
assertEquals(5 + 2, resp5.length); // 5 bytes + IAC EOR
assertEquals(DT_RESPONSE, resp5[0] & 0xFF);
assertEquals(0, resp5[1] & 0xFF);
assertEquals(RSF_POSITIVE_RESPONSE, resp5[2] & 0xFF);
assertEquals(0x56, resp5[3] & 0xFF);
assertEquals(0x78, resp5[4] & 0xFF);
}
@Test
public void testStartTlsOption46NegotiationAndElevation() {
config.setStartTlsEnabled(true);
fsm.onConnected();
connection.sentData.clear();
// Server sends DO STARTTLS (Option 46)
feedBytes(IAC, DO, TELOPT_STARTTLS);
// Client must reply WILL STARTTLS
assertEquals(1, connection.sentData.size());
assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_STARTTLS }, connection.sentData.get(0));
assertTrue(fsm.getMyOpts()[TELOPT_STARTTLS]);
connection.sentData.clear();
// Server sends SB STARTTLS 1 (TLS_FOLLOWS) SE
feedBytes(IAC, SB, TELOPT_STARTTLS, TLS_FOLLOWS, IAC, SE);
// Connection must have been upgraded to TLS
assertTrue(connection.tlsUpgraded, "Expected upgradeToTls() to be invoked on STARTTLS_FOLLOWS");
}
@Test
public void testRfc1572NewEnvironEmptySendReturnsAllVariables() {
config.setEnvironmentVariable("USER", "MAINFRAME_USER");
config.setEnvironmentVariable("SYSTEMTYPE", "MVS");
config.setUserVariable("IBM_EXPRESS_LOGON", "CERT_AUTH_ENABLED");
fsm.onConnected();
connection.sentData.clear();
// Server sends DO NEW_ENVIRON
feedBytes(IAC, DO, TELOPT_NEW_ENVIRON);
assertEquals(1, connection.sentData.size());
assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_NEW_ENVIRON }, connection.sentData.get(0));
connection.sentData.clear();
// Server sends SB NEW-ENVIRON SEND SE (empty query -> all variables)
feedBytes(IAC, SB, TELOPT_NEW_ENVIRON, TELQUAL_SEND, IAC, SE);
assertEquals(1, connection.sentData.size());
byte[] pkt = connection.sentData.get(0);
assertTrue(pkt.length >= 6);
assertEquals(IAC, pkt[0] & 0xFF);
assertEquals(SB, pkt[1] & 0xFF);
assertEquals(TELOPT_NEW_ENVIRON, pkt[2] & 0xFF);
assertEquals(TELQUAL_IS, pkt[3] & 0xFF);
String pktStr = new String(pkt);
assertTrue(pktStr.contains("USER"));
assertTrue(pktStr.contains("MAINFRAME_USER"));
assertTrue(pktStr.contains("SYSTEMTYPE"));
assertTrue(pktStr.contains("MVS"));
assertTrue(pktStr.contains("IBM_EXPRESS_LOGON"));
assertTrue(pktStr.contains("CERT_AUTH_ENABLED"));
}
@Test
public void testRfc1572NewEnvironSelectiveSend() throws Exception {
config.setEnvironmentVariable("USER", "ALICE");
config.setEnvironmentVariable("JOB", "DAILY_REPORT");
config.setUserVariable("SEC_TOKEN", "XYZ123");
fsm.onConnected();
connection.sentData.clear();
// Server sends SB NEW-ENVIRON SEND VAR "USER" USERVAR "SEC_TOKEN" SE
ByteArrayOutputStream query = new ByteArrayOutputStream();
query.write(IAC);
query.write(SB);
query.write(TELOPT_NEW_ENVIRON);
query.write(TELQUAL_SEND);
query.write(TELOBJ_VAR);
query.write("USER".getBytes());
query.write(TELOBJ_USERVAR);
query.write("SEC_TOKEN".getBytes());
query.write(IAC);
query.write(SE);
for (byte b : query.toByteArray()) {
fsm.feedByte(b & 0xFF);
}
assertEquals(1, connection.sentData.size());
String respStr = new String(connection.sentData.get(0));
assertTrue(respStr.contains("USER"));
assertTrue(respStr.contains("ALICE"));
assertTrue(respStr.contains("SEC_TOKEN"));
assertTrue(respStr.contains("XYZ123"));
assertFalse(respStr.contains("JOB"), "Unrequested variable JOB should not be sent");
}
@Test
public void testSendNewEnvironmentVariablesFromProperties() {
Properties props = new Properties();
props.setProperty("VAR_TEST", "VAL1");
props.setProperty("USERVAR_TOKEN", "VAL2");
connection.sentData.clear();
fsm.sendNewEnvironmentVariables(props);
assertEquals(1, connection.sentData.size());
byte[] pkt = connection.sentData.get(0);
assertEquals(IAC, pkt[0] & 0xFF);
assertEquals(SB, pkt[1] & 0xFF);
assertEquals(TELOPT_NEW_ENVIRON, pkt[2] & 0xFF);
assertEquals(TELQUAL_IS, pkt[3] & 0xFF);
String pktStr = new String(pkt);
assertTrue(pktStr.contains("VAR_TEST"));
assertTrue(pktStr.contains("VAL1"));
assertTrue(pktStr.contains("USERVAR_TOKEN"));
assertTrue(pktStr.contains("VAL2"));
}
@Test
public void testInboundSCSDataHandlingAndAutoPositiveResponse() throws Exception {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E with RESPONSES and SCS_CTL_CODES
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '8', '7', '-', '1',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SCS_CTL_CODES,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
List<byte[]> scsReceived = new ArrayList<>();
fsm.addSCSInboundListener(new SCSInboundListener() {
@Override
public void onSCSDataReceived(byte[] data, int offset, int length) {
byte[] copy = new byte[length];
System.arraycopy(data, offset, copy, 0, length);
scsReceived.add(copy);
}
});
connection.sentData.clear();
// Send DT_SCS_DATA (0x01) record with ALWAYS_RESPONSE
int seq = 0x00A1;
byte[] scsPayload = new byte[] { 0x15, 0x2B, (byte) 0xD2, 0x04, 0x15 }; // SCS orders
ByteArrayOutputStream scsRecord = new ByteArrayOutputStream();
scsRecord.write(DT_SCS_DATA);
scsRecord.write(0x00); // Request flag
scsRecord.write(RSF_ALWAYS_RESPONSE);
scsRecord.write((seq >> 8) & 0xFF);
scsRecord.write(seq & 0xFF);
scsRecord.write(scsPayload);
scsRecord.write(IAC);
scsRecord.write(EOR);
for (byte b : scsRecord.toByteArray()) {
fsm.feedByte(b & 0xFF);
}
// Verify SCS listener was notified with exact payload
assertEquals(1, scsReceived.size());
assertArrayEquals(scsPayload, scsReceived.get(0));
// Verify positive response was sent
boolean foundPositive = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length >= 8 &&
(pkt[0] & 0xFF) == DT_RESPONSE &&
(pkt[2] & 0xFF) == RSF_POSITIVE_RESPONSE &&
((((pkt[3] & 0xFF) << 8) | (pkt[4] & 0xFF)) == seq) &&
(pkt[5] & 0xFF) == POS_DEVICE_END) {
foundPositive = true;
}
}
assertTrue(foundPositive, "Expected positive response for DT_SCS_DATA with RSF_ALWAYS_RESPONSE");
}
@Test
public void testProcessSysReqMethod() {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SYSREQ,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
// Bind session
byte[] bindPacket = new byte[EH_SIZE + 35];
bindPacket[0] = DT_BIND_IMAGE;
bindPacket[1] = 0; bindPacket[2] = 0; bindPacket[3] = 0; bindPacket[4] = 1;
bindPacket[EH_SIZE + 24] = 0x02;
ByteArrayOutputStream bStream = new ByteArrayOutputStream();
bStream.write(bindPacket, 0, bindPacket.length);
bStream.write(IAC);
bStream.write(EOR);
for (byte b : bStream.toByteArray()) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
connection.sentData.clear();
// Call processSysReq()
fsm.processSysReq();
// State changes to CONNECTED_SSCP and IAC AO sent
assertEquals(ConnectionState.CONNECTED_SSCP, fsm.getConnectionState());
boolean foundAo = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 2 && (pkt[0] & 0xFF) == IAC && (pkt[1] & 0xFF) == AO) {
foundAo = true;
}
}
assertTrue(foundAo, "Expected IAC AO out-of-band telnet abort");
}
}
@@ -50,6 +50,9 @@ public class TelnetFSMTest {
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator()); screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator()); dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
fsm = new TelnetFSM(config, screenBuffer, dsProcessor); fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
haus.nightmare.lib3270j.input.InputProcessor inputProcessor =
new haus.nightmare.lib3270j.input.InputProcessor(screenBuffer, new EbcdicTranslator(), fsm);
dsProcessor.setInputProcessor(inputProcessor);
connection = new MockConnection(config, fsm); connection = new MockConnection(config, fsm);
fsm.setConnection(connection); fsm.setConnection(connection);
} }
@@ -294,4 +297,212 @@ public class TelnetFSMTest {
String resp3 = new String(lastPkt, 4, lastPkt.length - 6); String resp3 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3278-4-E", resp3); assertEquals("IBM-3278-4-E", resp3);
} }
@Test
public void testTn3270eNvtInboundAndOutbound() throws Exception {
fsm.onConnected();
connection.sentData.clear();
// Negotiate TN3270E without BIND-IMAGE
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
// Host sends DEVICE-TYPE SEND
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x08, 0x01, TelnetConstants.IAC, TelnetConstants.SE);
// Host sends DEVICE-TYPE IS IBM-3279-4-E
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
TelnetConstants.IAC, TelnetConstants.SE);
// Host sends FUNCTIONS IS (no BIND-IMAGE)
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x03, 0x04, TelnetConstants.IAC, TelnetConstants.SE);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
// Host sends DT_NVT_DATA (0x05) record with text "PROMPT: "
byte[] nvtRecord = {
0x05, // DT_NVT_DATA
0x00, // request flag
0x00, // response flag
0x00, 0x01, // seq 1
'P', 'R', 'O', 'M', 'P', 'T', ':', ' ',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
for (byte b : nvtRecord) {
fsm.feedByte(b & 0xFF);
}
// FSM must transition to CONNECTED_E_NVT
assertEquals(ConnectionState.CONNECTED_E_NVT, fsm.getConnectionState());
assertTrue(fsm.getConnectionState().isNvt());
// Verify screen buffer contains the NVT text
assertEquals('P', screenBuffer.getCell(0).ucs4);
assertEquals('R', screenBuffer.getCell(1).ucs4);
assertEquals('O', screenBuffer.getCell(2).ucs4);
assertEquals('M', screenBuffer.getCell(3).ucs4);
assertEquals('P', screenBuffer.getCell(4).ucs4);
assertEquals('T', screenBuffer.getCell(5).ucs4);
assertEquals(':', screenBuffer.getCell(6).ucs4);
assertEquals(' ', screenBuffer.getCell(7).ucs4);
assertEquals(8, screenBuffer.getCursorAddress());
// Test Outbound NVT sending in TN3270E mode
connection.sentData.clear();
fsm.sendNVTString("OK\n");
assertFalse(connection.sentData.isEmpty());
byte[] sentPkt = connection.sentData.get(connection.sentData.size() - 1);
// Sent packet should be TN3270E record: 5-byte header + "OK\r\n" + IAC EOR
assertEquals(5 + 4 + 2, sentPkt.length);
assertEquals(0x05, sentPkt[0]); // DT_NVT_DATA
assertEquals('O', sentPkt[5]);
assertEquals('K', sentPkt[6]);
assertEquals('\r', sentPkt[7]);
assertEquals('\n', sentPkt[8]);
assertEquals((byte) TelnetConstants.IAC, sentPkt[9]);
assertEquals((byte) TelnetConstants.EOR, sentPkt[10]);
}
@Test
public void testPlainTelnetNvtStreamingAndLocalEcho() throws Exception {
fsm.onConnected();
config.setNvtLocalEcho(true);
// Receiving raw bytes in TELNET_PENDING automatically transitions to CONNECTED_NVT
feedBytes('L', 'O', 'G', 'I', 'N', '>');
assertEquals(ConnectionState.CONNECTED_NVT, fsm.getConnectionState());
assertEquals('L', screenBuffer.getCell(0).ucs4);
assertEquals('>', screenBuffer.getCell(5).ucs4);
// Sending NVT data with local echo enabled
connection.sentData.clear();
fsm.sendNVTString("TEST");
// Raw bytes sent over TCP
assertEquals(1, connection.sentData.size());
byte[] sent = connection.sentData.get(0);
assertArrayEquals("TEST".getBytes(), sent);
// Local echo placed "TEST" on screen starting at address 6
assertEquals('T', screenBuffer.getCell(6).ucs4);
assertEquals('E', screenBuffer.getCell(7).ucs4);
assertEquals('S', screenBuffer.getCell(8).ucs4);
assertEquals('T', screenBuffer.getCell(9).ucs4);
assertEquals(10, screenBuffer.getCursorAddress());
}
@Test
public void testVmConmode3270TransitionTn3270e() throws Exception {
fsm.onConnected();
connection.sentData.clear();
// 1. Negotiate TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x08, 0x01, TelnetConstants.IAC, TelnetConstants.SE);
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
TelnetConstants.IAC, TelnetConstants.SE);
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x03, 0x04, TelnetConstants.IAC, TelnetConstants.SE);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
// 2. VM console in line-mode sends DT_NVT_DATA (e.g. CP prompt)
byte[] nvtRecord = {
0x05, 0x00, 0x00, 0x00, 0x01,
'C', 'P', ' ', 'R', 'E', 'A', 'D', 'Y', '\r', '\n',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
for (byte b : nvtRecord) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_E_NVT, fsm.getConnectionState());
assertEquals(TelnetFSM.TN3270ESubmode.E_NVT, fsm.getTn3270eSubmode());
// 3. VM executes "cp term conmode 3270" and sends 3270 full screen (DT_3270_DATA)
EbcdicTranslator trans = new EbcdicTranslator();
String banner = "z/VM 3270 FULLSCREEN";
byte[] stream3270 = new byte[5 + 5 + banner.length() + 2];
// 5-byte TN3270E header
stream3270[0] = 0x00; // DT_3270_DATA
stream3270[1] = 0x00;
stream3270[2] = 0x00;
stream3270[3] = 0x00; stream3270[4] = 0x02; // seq 2
// 3270 Orders
stream3270[5] = (byte) 0xF5; // EraseWrite
stream3270[6] = (byte) 0xC3; // WCC
stream3270[7] = 0x11; // SBA
stream3270[8] = 0x40; stream3270[9] = 0x40; // pos 0
for (int i = 0; i < banner.length(); i++) {
stream3270[10 + i] = (byte) trans.unicodeToEbcdic(banner.charAt(i));
}
stream3270[stream3270.length - 2] = (byte) TelnetConstants.IAC;
stream3270[stream3270.length - 1] = (byte) TelnetConstants.EOR;
for (byte b : stream3270) fsm.feedByte(b & 0xFF);
// Verify transition from E_NVT to E_3270 (CONNECTED_TN3270E)
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
assertEquals(TelnetFSM.TN3270ESubmode.E_3270, fsm.getTn3270eSubmode());
assertFalse(fsm.getConnectionState().isNvt());
// Verify screen displays 3270 content
assertEquals('z', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
assertEquals('/', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
assertEquals('V', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
assertEquals('M', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
// 4. Outbound 3270 AID transmission should now use DT_3270_DATA
connection.sentData.clear();
dsProcessor.getInputProcessor().sendAid(0x7D);
assertFalse(connection.sentData.isEmpty());
byte[] sentAid = connection.sentData.get(connection.sentData.size() - 1);
assertEquals(0x00, sentAid[0]); // DT_3270_DATA header
assertEquals((byte) 0x7D, sentAid[5]); // Enter AID code
}
@Test
public void testVmConmode3270TransitionPlainTelnet() throws Exception {
fsm.onConnected();
config.setTn3270eEnabled(false);
// 1. Connected in line-mode (NVT)
feedBytes('C', 'P', '>');
assertEquals(ConnectionState.CONNECTED_NVT, fsm.getConnectionState());
// 2. VM sends BINARY and EOR options upon CONMODE 3270
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
// Verify transition to CONNECTED_3270
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
// 3. VM sends 3270 data stream
EbcdicTranslator trans = new EbcdicTranslator();
String msg = "VM 3270";
byte[] rawStream = new byte[5 + msg.length() + 2];
rawStream[0] = (byte) 0xF5; // EraseWrite
rawStream[1] = (byte) 0xC3; // WCC
rawStream[2] = 0x11; // SBA
rawStream[3] = 0x40; rawStream[4] = 0x40;
for (int i = 0; i < msg.length(); i++) {
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
}
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
assertEquals('V', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
assertEquals('M', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
}
} }