5 Commits

Author SHA1 Message Date
rudi db6043cb39 Handle HeadlessException in UI tests for automated testing
Release j3270 / Build & Publish Release (push) Successful in 1m28s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m31s
2026-09-01 10:17:29 -04:00
rudi 161364a95f Overnight churnings
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 1m23s
Release j3270 / Build & Publish Release (push) Failing after 1m15s
2026-09-01 09:45:40 -04:00
rudi a56aa5f059 More NVT stuff
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m24s
2026-08-31 15:54:26 -04:00
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
132 changed files with 16717 additions and 1313 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ public class TestRunner {
public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("haus.nightmare.lib3270j")
selectPackage("haus.nightmare")
)
.build();
@@ -53,6 +53,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
buildUI();
buildMenuBar();
ThemeManager.addThemeChangeListener(this::onThemeChanged);
pack();
setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400));
@@ -97,10 +99,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
getContentPane().add(statusBar, BorderLayout.SOUTH);
}
private void onThemeChanged(UITheme theme) {
buildMenuBar();
statusBar.applyTheme(theme);
ThemeManager.applyThemeToWindow(this);
terminalPanel.repaint();
}
private void buildMenuBar() {
JMenuBar menuBar = new JMenuBar();
menuBar.setBackground(new Color(30, 30, 30));
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
ThemeManager.styleMenuBar(menuBar);
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
@@ -137,12 +145,37 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 3. View menu
JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2), true));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2), true));
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
terminalPanel.setFontSize(16);
terminalPanel.guardedPack();
}));
}, true));
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
@@ -174,8 +207,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
for (String cpItem : codePages) {
String cpId = cpItem.split(" -")[0].trim();
JMenuItem cpMi = new JMenuItem(cpItem);
cpMi.setBackground(new Color(40, 40, 40));
cpMi.setForeground(new Color(200, 200, 200));
ThemeManager.styleMenuItem(cpMi);
cpMi.addActionListener(e -> changeCodePage(cpId));
cpMenu.add(cpMi);
}
@@ -185,8 +217,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JMenu gfxMenu = createMenu("Graphics Mode");
for (GraphicsMode gm : GraphicsMode.values()) {
JMenuItem gmMi = new JMenuItem(gm.name());
gmMi.setBackground(new Color(40, 40, 40));
gmMi.setForeground(new Color(200, 200, 200));
ThemeManager.styleMenuItem(gmMi);
gmMi.addActionListener(e -> changeGraphicsMode(gm));
gfxMenu.add(gmMi);
}
@@ -203,17 +234,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.sendEnter();
terminalPanel.repaint();
}
}));
}, true));
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
if (client != null)
client.sendClear();
terminalPanel.repaint();
}));
}, true));
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
if (client != null)
client.reset();
terminalPanel.repaint();
}));
}, true));
actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> {
if (client != null && client.getConnectionState().isFullSession()) {
client.eraseInput();
@@ -241,7 +272,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
menuBar.add(actionsMenu);
// 5. Help menu
@@ -257,14 +288,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private JMenu createMenu(String name) {
JMenu menu = new JMenu(name);
menu.setForeground(new Color(200, 200, 200));
return menu;
return ThemeManager.styleMenu(menu);
}
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
JMenuItem item = new JMenuItem(name);
item.setBackground(new Color(40, 40, 40));
item.setForeground(new Color(200, 200, 200));
ThemeManager.styleMenuItem(item);
if (mnemonic > 0) {
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
@@ -677,25 +706,25 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"Insert — Toggle insert mode\n" +
"Escape — Reset\n" +
"PageUp/Down — PF7/PF8\n" +
"Alt+C / Ctrl+K — Clear\n" +
"Alt+C / Alt+K — Clear\n" +
"Alt+E — Erase Input\n" +
"Alt+A — Attention\n" +
"Alt+S — System Request\n" +
"Alt+Q — Cursor Select\n" +
"Alt+L — Toggle Light Pen\n" +
"Alt+T — File Transfer\n" +
"Alt+=/-/0 — Font size +/-/reset\n" +
"Cmd/Ctrl+F — Find on Screen\n" +
"Cmd/Ctrl+G / F3— Find Next\n" +
"Cmd/Ctrl+T — File Transfer\n" +
"Cmd/Ctrl+D — Disconnect\n" +
"Cmd/Ctrl+Q — Quit\n" +
"Cmd/Ctrl+=/-/0 — Font size +/-/reset";
"Cmd/Ctrl+Q — Quit";
JTextArea area = new JTextArea(text);
area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp);
sp.setPreferredSize(new Dimension(380, 430));
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);
area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp);
sp.setPreferredSize(new Dimension(440, 430));
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;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
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;
} 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;
} 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;
} 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;
} else if ("--tn3270e".equals(args[i])) {
} else if ("--tn3270e".equals(arg)) {
cliTn3270e = true;
} else if (args[i].startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(args[i].substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
} else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(args[i])) {
} else if ("--no-graphics".equals(arg)) {
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];
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} 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("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
}
globalRoot.setLevel(Level.ALL);
java.util.logging.Filter appFilter = record -> {
String name = record.getLoggerName();
return name != null && (name.startsWith("haus.nightmare.lib3270j") || name.startsWith("haus.nightmare.j3270"));
};
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(logLevel);
consoleHandler.setLevel(Level.ALL);
consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler);
@@ -837,6 +868,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} catch (Exception e) {
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.awt.application.name", "j3270");
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.config;
import haus.nightmare.j3270.ui.UITheme;
import java.util.prefs.Preferences;
import java.awt.Color;
import java.io.*;
@@ -17,6 +18,15 @@ public class Settings {
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() {
return prefs.get("fontFamily", "Monospaced");
}
@@ -229,6 +239,11 @@ public class Settings {
switch (key) {
case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "javaUiTheme":
case "theme":
case "uiTheme":
setJavaUiTheme(UITheme.fromString(value));
break;
default:
log.warning("Unknown appearance key: " + key);
}
@@ -322,6 +337,7 @@ public class Settings {
// [appearance]
w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize());
w.println();
@@ -1,6 +1,7 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants;
@@ -88,6 +89,7 @@ public class FileTransferDialog extends JDialog {
// Local File
localFileField = new JTextField(20);
browseLocalButton = new JButton("Browse...");
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
browseLocalButton.addActionListener(e -> browseLocalFile());
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
localPanel.setOpaque(false);
@@ -98,6 +100,7 @@ public class FileTransferDialog extends JDialog {
// Host File
hostFileField = new JTextField(20);
browseHostButton = new JButton("Browse Host...");
ThemeManager.styleButton(browseHostButton, ThemeManager.ButtonVariant.DEFAULT);
browseHostButton.addActionListener(e -> browseHostDirectory());
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
hostPanel.setOpaque(false);
@@ -154,10 +157,7 @@ public class FileTransferDialog extends JDialog {
// Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(BorderFactory.createTitledBorder(
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.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
hostOptsPanel.add(new JLabel("RECFM:"));
recfmField = new JTextField(5);
@@ -181,10 +181,7 @@ public class FileTransferDialog extends JDialog {
// CMS options
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
vmOptsPanel.setOpaque(false);
vmOptsPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"CMS Options"));
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
vmOptsPanel.setBorder(ThemeManager.createTitledBorder("CMS Options"));
optionsField = new JTextField(20);
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
@@ -200,9 +197,11 @@ public class FileTransferDialog extends JDialog {
buttonPanel.setOpaque(false);
transferButton = new JButton("Start Transfer");
ThemeManager.styleButton(transferButton, ThemeManager.ButtonVariant.PRIMARY);
transferButton.addActionListener(e -> startTransfer());
cancelButton = new JButton("Close");
ThemeManager.styleButton(cancelButton, ThemeManager.ButtonVariant.CANCEL);
cancelButton.addActionListener(e -> dispose());
buttonPanel.add(cancelButton);
@@ -212,7 +211,7 @@ public class FileTransferDialog extends JDialog {
setContentPane(mainPanel);
applyTheme(mainPanel);
ThemeManager.applyThemeToWindow(this);
updateOptionStates();
}
@@ -34,24 +34,21 @@ public class ConnectDialog extends JDialog {
private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.insets = new Insets(5, 6, 5, 6);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14);
Color fg = new Color(200, 200, 200);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Host
gbc.gridx = 0;
gbc.gridy = 0;
JLabel hostLabel = new JLabel("Host:");
hostLabel.setForeground(fg);
hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
hostField = createDarkField(20);
hostField = createField(20);
mainPanel.add(hostField, gbc);
// Port
@@ -59,12 +56,11 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 1;
gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:");
portLabel.setForeground(fg);
portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
portField = createDarkField(6);
portField = createField(6);
portField.setText("23");
mainPanel.add(portField, gbc);
@@ -73,16 +69,14 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 2;
gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:");
modelLabel.setForeground(fg);
modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values());
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));
ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc);
// LU Name
@@ -90,12 +84,11 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 3;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setForeground(fg);
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
luField = createDarkField(12);
luField = createField(12);
mainPanel.add(luField, gbc);
// Graphics Mode
@@ -103,16 +96,14 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 4;
gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setForeground(fg);
graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
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));
ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc);
// Code Page
@@ -120,7 +111,6 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 5;
gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setForeground(fg);
cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc);
gbc.gridx = 1;
@@ -139,14 +129,27 @@ public class ConnectDialog extends JDialog {
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish)",
"1026 - Turkey (Turkish Latin-5)",
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual",
"424 - Hebrew (with Lowercase)",
"803 - Hebrew Old / Standard",
"838 - Thai",
"1160 - Thai (Euro \u20AC)",
"1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine",
"1154 - Cyrillic Multilingual (Euro \u20AC)",
"880 - Cyrillic Russian",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended Mixed DBCS",
"933 - Korean Mixed DBCS"
};
codePageCombo = new JComboBox<>(commonCodePages);
@@ -158,9 +161,8 @@ public class ConnectDialog extends JDialog {
break;
}
}
codePageCombo.setBackground(new Color(45, 45, 45));
codePageCombo.setForeground(fg);
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox
@@ -168,10 +170,8 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 6;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
tlsCheckBox.setBackground(new Color(30, 30, 30));
tlsCheckBox.setForeground(fg);
ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setFont(labelFont);
tlsCheckBox.setFocusPainted(false);
tlsCheckBox.addActionListener(e -> {
boolean isTls = tlsCheckBox.isSelected();
verifyCertCheckBox.setEnabled(isTls);
@@ -188,38 +188,31 @@ public class ConnectDialog extends JDialog {
gbc.gridx = 1;
gbc.gridy = 7;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false);
verifyCertCheckBox.setFocusPainted(false);
mainPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 8;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
tn3270eCheckBox.setForeground(fg);
ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
tn3270eCheckBox.setFocusPainted(false);
mainPanel.add(tn3270eCheckBox, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setBackground(new Color(30, 30, 30));
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
JButton connectBtn = new JButton("Connect");
connectBtn.setBackground(new Color(50, 120, 50));
connectBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.setFont(labelFont);
connectBtn.addActionListener(e -> onConnect());
JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(fg);
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setFont(labelFont);
cancelBtn.addActionListener(e -> {
confirmed = false;
@@ -235,20 +228,16 @@ public class ConnectDialog extends JDialog {
mainPanel.add(buttonPanel, gbc);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect
getRootPane().setDefaultButton(connectBtn);
}
private JTextField createDarkField(int cols) {
private JTextField createField(int 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.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
ThemeManager.styleTextField(field);
return field;
}
@@ -28,12 +28,6 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
private JTable table;
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) {
super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client;
@@ -61,19 +55,16 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(DARK_BG);
// Top info bar
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen");
countLabel.setForeground(DARK_FG);
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh");
refreshBtn.setBackground(new Color(55, 55, 55));
refreshBtn.setForeground(DARK_FG);
ThemeManager.styleButton(refreshBtn, ThemeManager.ButtonVariant.DEFAULT);
refreshBtn.addActionListener(e -> refreshFields());
topPanel.add(refreshBtn, BorderLayout.EAST);
mainPanel.add(topPanel, BorderLayout.NORTH);
@@ -91,18 +82,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
};
table = new JTable(tableModel);
table.setBackground(DARK_FIELD_BG);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
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));
// Column widths
@@ -125,8 +110,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
});
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(DARK_BORDER));
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom panel
@@ -134,8 +118,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
bottomPanel.setOpaque(false);
JButton jumpBtn = new JButton("Jump to Selected Field");
jumpBtn.setBackground(new Color(50, 100, 160));
jumpBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(jumpBtn, ThemeManager.ButtonVariant.PRIMARY);
jumpBtn.addActionListener(e -> {
int row = table.getSelectedRow();
if (row >= 0) {
@@ -144,8 +127,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
});
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(55, 55, 55));
closeBtn.setForeground(DARK_FG);
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.addActionListener(e -> dispose());
bottomPanel.add(jumpBtn);
@@ -153,6 +135,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
}
public synchronized void refreshFields() {
@@ -42,7 +42,6 @@ public class FindDialog extends JDialog {
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
// Form
JPanel formPanel = new JPanel(new GridBagLayout());
@@ -51,14 +50,12 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Search text
gbc.gridx = 0;
gbc.gridy = 0;
JLabel findLabel = new JLabel("Find what:");
findLabel.setForeground(fg);
findLabel.setFont(labelFont);
formPanel.add(findLabel, gbc);
@@ -66,23 +63,16 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0;
searchField = new JTextField(20);
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.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(70, 70, 70)),
BorderFactory.createEmptyBorder(3, 6, 3, 6)));
ThemeManager.styleTextField(searchField);
formPanel.add(searchField, gbc);
// Options
gbc.gridx = 1;
gbc.gridy = 1;
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
matchCaseCheck.setForeground(fg);
matchCaseCheck.setOpaque(false);
ThemeManager.styleCheckBox(matchCaseCheck);
matchCaseCheck.setFont(labelFont);
matchCaseCheck.setFocusPainted(false);
formPanel.add(matchCaseCheck, gbc);
// Direction
@@ -91,16 +81,11 @@ public class FindDialog extends JDialog {
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false);
JLabel dirLabel = new JLabel("Direction: ");
dirLabel.setForeground(fg);
dirLabel.setFont(labelFont);
forwardRadio = new JRadioButton("Down", lastForward);
backwardRadio = new JRadioButton("Up", !lastForward);
forwardRadio.setForeground(fg);
backwardRadio.setForeground(fg);
forwardRadio.setOpaque(false);
backwardRadio.setOpaque(false);
forwardRadio.setFocusPainted(false);
backwardRadio.setFocusPainted(false);
ThemeManager.styleRadioButton(forwardRadio);
ThemeManager.styleRadioButton(backwardRadio);
ButtonGroup bg = new ButtonGroup();
bg.add(forwardRadio);
@@ -116,7 +101,7 @@ public class FindDialog extends JDialog {
gbc.gridy = 3;
gbc.gridwidth = 2;
statusLabel = new JLabel(" ");
statusLabel.setForeground(new Color(255, 120, 120));
statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
formPanel.add(statusLabel, gbc);
@@ -127,14 +112,12 @@ public class FindDialog extends JDialog {
buttonPanel.setOpaque(false);
JButton findNextBtn = new JButton("Find Next");
findNextBtn.setBackground(new Color(60, 63, 65));
findNextBtn.setForeground(fg);
ThemeManager.styleButton(findNextBtn, ThemeManager.ButtonVariant.PRIMARY);
findNextBtn.setFont(labelFont);
findNextBtn.addActionListener(e -> findNext());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 63, 65));
closeBtn.setForeground(fg);
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
@@ -144,6 +127,7 @@ public class FindDialog extends JDialog {
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
getRootPane().setDefaultButton(findNextBtn);
searchField.addKeyListener(new KeyAdapter() {
@@ -49,9 +49,7 @@ public class HostDirectoryDialog extends JDialog {
private void buildUI(String initialQuery) {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
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);
// Top Query bar
@@ -62,7 +60,6 @@ public class HostDirectoryDialog extends JDialog {
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel envLabel = new JLabel("System:");
envLabel.setForeground(fg);
envLabel.setFont(font);
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});
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
hostTypeCombo.setBackground(new Color(50, 50, 50));
hostTypeCombo.setForeground(fg);
ThemeManager.styleComboBox(hostTypeCombo);
topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:");
queryLabel.setForeground(fg);
queryLabel.setFont(font);
topPanel.add(queryLabel, gbc);
gbc.gridx = 3;
gbc.weightx = 1.0;
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.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(70, 70, 70)),
new EmptyBorder(3, 5, 3, 5)));
ThemeManager.styleTextField(queryField);
topPanel.add(queryField, gbc);
gbc.gridx = 4;
gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse");
parseBtn.setBackground(new Color(55, 55, 55));
parseBtn.setForeground(fg);
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
parseBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery());
topPanel.add(parseBtn, gbc);
@@ -106,18 +95,12 @@ public class HostDirectoryDialog extends JDialog {
// Table
tableModel = new DefaultTableModel();
table = new JTable(tableModel);
table.setBackground(new Color(45, 45, 45));
table.setForeground(fg);
table.setSelectionBackground(new Color(75, 110, 175));
table.setSelectionForeground(Color.WHITE);
table.setGridColor(new Color(65, 65, 65));
ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20);
JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(fg);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
table.addMouseListener(new MouseAdapter() {
@@ -130,8 +113,7 @@ public class HostDirectoryDialog extends JDialog {
});
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(new Color(65, 65, 65)));
scrollPane.getViewport().setBackground(new Color(45, 45, 45));
ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom
@@ -139,7 +121,7 @@ public class HostDirectoryDialog extends JDialog {
bottomPanel.setOpaque(false);
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));
bottomPanel.add(statusLabel, BorderLayout.WEST);
@@ -147,20 +129,17 @@ public class HostDirectoryDialog extends JDialog {
btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen");
pasteScreenBtn.setBackground(new Color(50, 50, 50));
pasteScreenBtn.setForeground(fg);
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteScreenBtn.setFont(font);
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton selectBtn = new JButton("Select Dataset");
selectBtn.setBackground(new Color(50, 120, 50));
selectBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
selectBtn.setFont(font);
selectBtn.addActionListener(e -> onConfirmSelection());
JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(fg);
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setFont(font);
cancelBtn.addActionListener(e -> {
confirmed = false;
@@ -176,6 +155,7 @@ public class HostDirectoryDialog extends JDialog {
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Initial setup
setupTableColumns();
@@ -45,10 +45,6 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool Display
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) {
super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent;
@@ -65,14 +61,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(DARK_BG);
// Top: Configuration Panel
JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false);
topPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(65, 65, 65)), "Printer Session Configuration"));
((TitledBorder) topPanel.getBorder()).setTitleColor(DARK_FG);
topPanel.setBorder(ThemeManager.createTitledBorder("Printer Session Configuration"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 3, 4);
@@ -83,60 +76,59 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Host / Port
gbc.gridx = 0; gbc.gridy = 0;
JLabel hLbl = new JLabel("Host:");
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont);
hLbl.setFont(labelFont);
topPanel.add(hLbl, gbc);
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);
gbc.gridx = 2; gbc.weightx = 0;
JLabel pLbl = new JLabel("Port:");
pLbl.setForeground(DARK_FG); pLbl.setFont(labelFont);
pLbl.setFont(labelFont);
topPanel.add(pLbl, gbc);
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);
// Printer LU / Display LU
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
JLabel pluLbl = new JLabel("Printer LU:");
pluLbl.setForeground(DARK_FG); pluLbl.setFont(labelFont);
pluLbl.setFont(labelFont);
topPanel.add(pluLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0;
printerLuField = createDarkField("", 10);
printerLuField = createField("", 10);
topPanel.add(printerLuField, gbc);
gbc.gridx = 2; gbc.weightx = 0;
JLabel assocLbl = new JLabel("Assoc LU:");
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont);
assocLbl.setFont(labelFont);
topPanel.add(assocLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5;
displayLuField = createDarkField("", 10);
displayLuField = createField("", 10);
topPanel.add(displayLuField, gbc);
// CodePage & TLS
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
JLabel cpLbl = new JLabel("CodePage:");
cpLbl.setForeground(DARK_FG); cpLbl.setFont(labelFont);
cpLbl.setFont(labelFont);
topPanel.add(cpLbl, gbc);
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.setBackground(DARK_FIELD_BG);
codePageCombo.setForeground(DARK_FG);
ThemeManager.styleComboBox(codePageCombo);
topPanel.add(codePageCombo, gbc);
gbc.gridx = 2; gbc.gridwidth = 2;
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
tlsPanel.setOpaque(false);
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.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false);
ThemeManager.styleCheckBox(verifyCertCheck);
tlsPanel.add(tlsCheck);
tlsPanel.add(verifyCertCheck);
topPanel.add(tlsPanel, gbc);
@@ -144,23 +136,22 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Destination Type & Target
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
JLabel destLbl = new JLabel("Destination:");
destLbl.setForeground(DARK_FG); destLbl.setFont(labelFont);
destLbl.setFont(labelFont);
topPanel.add(destLbl, gbc);
gbc.gridx = 1;
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
destinationCombo.setBackground(DARK_FIELD_BG);
destinationCombo.setForeground(DARK_FG);
ThemeManager.styleComboBox(destinationCombo);
topPanel.add(destinationCombo, gbc);
gbc.gridx = 2;
JLabel tgtLbl = new JLabel("Target Path:");
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont);
tgtLbl.setFont(labelFont);
topPanel.add(tgtLbl, gbc);
gbc.gridx = 3;
targetField = createDarkField("printer_output.txt", 12);
targetField = createField("printer_output.txt", 12);
topPanel.add(targetField, gbc);
// Connect / Disconnect Buttons
@@ -169,13 +160,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
connBtnPan.setOpaque(false);
connectBtn = new JButton("Start Printer Session");
connectBtn.setBackground(new Color(50, 120, 50));
connectBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.addActionListener(e -> startPrinterSession());
disconnectBtn = new JButton("Stop Session");
disconnectBtn.setBackground(new Color(120, 50, 50));
disconnectBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(disconnectBtn, ThemeManager.ButtonVariant.DANGER);
disconnectBtn.setEnabled(false);
disconnectBtn.addActionListener(e -> stopPrinterSession());
@@ -188,9 +177,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Center: Spool Area & Status
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
centerPanel.setOpaque(false);
centerPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(65, 65, 65)), "Printer Spool & Status"));
((TitledBorder) centerPanel.getBorder()).setTitleColor(DARK_FG);
centerPanel.setBorder(ThemeManager.createTitledBorder("Printer Spool & Status"));
// Status Header
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));
statusLabel = new JLabel("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180));
statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont);
sessionTypeLabel = new JLabel("Session: -");
sessionTypeLabel.setForeground(DARK_FG);
sessionTypeLabel.setFont(labelFont);
pagesLabel = new JLabel("Pages: 0");
pagesLabel.setForeground(DARK_FG);
pagesLabel.setFont(labelFont);
bytesLabel = new JLabel("Bytes: 0");
bytesLabel.setForeground(DARK_FG);
bytesLabel.setFont(labelFont);
statusHeader.add(statusLabel);
@@ -221,14 +205,12 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area
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.setEditable(false);
ThemeManager.styleTextArea(spoolArea);
JScrollPane spoolScroll = new JScrollPane(spoolArea);
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60)));
ThemeManager.styleScrollPane(spoolScroll);
centerPanel.add(spoolScroll, BorderLayout.CENTER);
mainPanel.add(centerPanel, BorderLayout.CENTER);
@@ -238,8 +220,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
bottomPanel.setOpaque(false);
JButton clearSpoolBtn = new JButton("Clear Spool");
clearSpoolBtn.setBackground(new Color(50, 50, 50));
clearSpoolBtn.setForeground(DARK_FG);
ThemeManager.styleButton(clearSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
clearSpoolBtn.addActionListener(e -> {
spoolArea.setText("");
if (printerSession != null && printerSession.getPD() != null) {
@@ -250,18 +231,15 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
});
JButton saveSpoolBtn = new JButton("Save Spool As...");
saveSpoolBtn.setBackground(new Color(50, 50, 50));
saveSpoolBtn.setForeground(DARK_FG);
ThemeManager.styleButton(saveSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
saveSpoolBtn.addActionListener(e -> saveSpool());
JButton printSpoolBtn = new JButton("Print Spool...");
printSpoolBtn.setBackground(new Color(50, 100, 160));
printSpoolBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(printSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
printSpoolBtn.addActionListener(e -> printSpool());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60));
closeBtn.setForeground(DARK_FG);
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.addActionListener(e -> dispose());
bottomPanel.add(clearSpoolBtn);
@@ -273,17 +251,13 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
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);
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
tf.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(65, 65, 65)),
new EmptyBorder(2, 4, 2, 4)));
ThemeManager.styleTextField(tf);
return tf;
}
@@ -34,9 +34,7 @@ public class ScriptDialog extends JDialog {
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
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);
// Header
@@ -44,31 +42,24 @@ public class ScriptDialog extends JDialog {
topPanel.setOpaque(false);
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>");
descLabel.setForeground(new Color(180, 180, 180));
descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Script area
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.setLineWrap(true);
scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea);
JScrollPane scrollPane = new JScrollPane(scriptArea);
scrollPane.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 2, 2, 2)));
ThemeManager.styleScrollPane(scrollPane);
// Mnemonic helper buttons
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
tokenPanel.setOpaque(false);
tokenPanel.setBorder(BorderFactory.createTitledBorder(
new LineBorder(new Color(60, 60, 60)), "Insert Keystroke Token"));
((TitledBorder) tokenPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
tokenPanel.setBorder(ThemeManager.createTitledBorder("Insert Keystroke Token"));
String[] tokens = {
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
@@ -84,8 +75,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) {
JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
btn.setBackground(new Color(50, 50, 50));
btn.setForeground(fg);
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4));
btn.addActionListener(e -> insertToken(token));
@@ -103,7 +93,7 @@ public class ScriptDialog extends JDialog {
bottomPanel.setOpaque(false);
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));
bottomPanel.add(statusLabel, BorderLayout.WEST);
@@ -111,26 +101,22 @@ public class ScriptDialog extends JDialog {
buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load Script...");
loadBtn.setBackground(new Color(50, 50, 50));
loadBtn.setForeground(fg);
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadScript());
JButton saveBtn = new JButton("Save Script...");
saveBtn.setBackground(new Color(50, 50, 50));
saveBtn.setForeground(fg);
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveScript());
JButton runBtn = new JButton("Execute");
runBtn.setBackground(new Color(50, 120, 50));
runBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(runBtn, ThemeManager.ButtonVariant.PRIMARY);
runBtn.setFont(labelFont);
runBtn.addActionListener(e -> executeScript());
JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60));
closeBtn.setForeground(fg);
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
@@ -144,6 +130,7 @@ public class ScriptDialog extends JDialog {
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
}
private void insertToken(String token) {
@@ -19,16 +19,8 @@ public class SettingsDialog extends JDialog {
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
private JComboBox<UITheme> uiThemeBox;
private JComboBox<String> fontBox;
private JSpinner fontSizeSpinner;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
@@ -51,22 +43,30 @@ public class SettingsDialog extends JDialog {
this.parentApp = parent;
initComponents();
setSize(550, 450);
setSize(560, 480);
setLocationRelativeTo(parent);
}
private void initComponents() {
JTabbedPane tabbedPane = new JTabbedPane();
ThemeManager.styleTabbedPane(tabbedPane);
tabbedPane.addTab("Appearance", createAppearancePanel());
tabbedPane.addTab("Behavior", createBehaviorPanel());
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 btnOk = new JButton("OK");
ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
JButton btnApply = new JButton("Apply");
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
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) -> {
exportConfig();
@@ -96,132 +96,11 @@ public class SettingsDialog extends JDialog {
getContentPane().add(tabbedPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
// Apply dark theme to all components for cross-platform readability
applyDarkTheme(getContentPane());
applyDarkTheme(tabbedPane);
applyDarkTheme(buttonPanel);
getContentPane().setBackground(DARK_BG);
ThemeManager.applyThemeToWindow(this);
}
// ========== 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) {
if (comp instanceof JTabbedPane) {
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);
}
}
ThemeManager.applyTheme(comp);
}
// ========== Tab Panels ==========
@@ -232,9 +111,22 @@ public class SettingsDialog extends JDialog {
gbc.insets = new Insets(10, 10, 10, 10);
gbc.anchor = GridBagConstraints.WEST;
// Font family
// UI Theme
gbc.gridx = 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:");
panel.add(fontLabel, gbc);
@@ -254,7 +146,7 @@ public class SettingsDialog extends JDialog {
// Font size
gbc.gridx = 0;
gbc.gridy = 1;
gbc.gridy = 2;
gbc.fill = GridBagConstraints.NONE;
JLabel sizeLabel = new JLabel("Font Size:");
panel.add(sizeLabel, gbc);
@@ -267,7 +159,7 @@ public class SettingsDialog extends JDialog {
// Graphics Mode
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridy = 3;
gbc.fill = GridBagConstraints.NONE;
JLabel graphicsLabel = new JLabel("Graphics Mode:");
panel.add(graphicsLabel, gbc);
@@ -279,7 +171,7 @@ public class SettingsDialog extends JDialog {
panel.add(graphicsModeBox, gbc);
// Fill remaining space
gbc.gridy = 3;
gbc.gridy = 4;
gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc);
@@ -429,6 +321,7 @@ public class SettingsDialog extends JDialog {
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnResetColors = new JButton("Reset to Defaults");
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
btnResetColors.addActionListener(e -> {
for (int i=0; i<16; 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++) {
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.");
});
resetPanel.add(btnResetColors);
@@ -498,6 +389,7 @@ public class SettingsDialog extends JDialog {
}
JTable table = new JTable(keymapModel);
ThemeManager.styleTable(table);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setFillsViewportHeight(true);
@@ -510,9 +402,8 @@ public class SettingsDialog extends JDialog {
captureDialog.setSize(320, 100);
captureDialog.setLocationRelativeTo(this);
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
lbl.setForeground(DARK_FG);
captureDialog.getContentPane().setBackground(DARK_BG);
captureDialog.add(lbl);
ThemeManager.applyThemeToWindow(captureDialog);
captureDialog.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
@@ -532,6 +423,7 @@ public class SettingsDialog extends JDialog {
// Replace Binding — sets the action to exactly one new key
JButton btnReplace = new JButton("Replace Binding");
ThemeManager.styleButton(btnReplace, ThemeManager.ButtonVariant.DEFAULT);
btnReplace.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
@@ -545,6 +437,7 @@ public class SettingsDialog extends JDialog {
// Add Binding — appends an additional key to the existing binding(s)
JButton btnAdd = new JButton("Add Binding");
ThemeManager.styleButton(btnAdd, ThemeManager.ButtonVariant.DEFAULT);
btnAdd.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
@@ -565,6 +458,7 @@ public class SettingsDialog extends JDialog {
// Remove Last — removes the last comma-separated binding entry
JButton btnRemoveLast = new JButton("Remove Last");
ThemeManager.styleButton(btnRemoveLast, ThemeManager.ButtonVariant.DEFAULT);
btnRemoveLast.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
@@ -585,6 +479,7 @@ public class SettingsDialog extends JDialog {
// Unbind All — clears all bindings for the action
JButton btnUnbind = new JButton("Unbind All");
ThemeManager.styleButton(btnUnbind, ThemeManager.ButtonVariant.DEFAULT);
btnUnbind.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
@@ -596,6 +491,7 @@ public class SettingsDialog extends JDialog {
// Reset Keymaps — restore all defaults
JButton btnReset = new JButton("Reset All");
ThemeManager.styleButton(btnReset, ThemeManager.ButtonVariant.DEFAULT);
btnReset.addActionListener(e -> {
for (int row = 0; row < keymapModel.getRowCount(); row++) {
String action = (String) keymapModel.getValueAt(row, 0);
@@ -628,6 +524,12 @@ public class SettingsDialog extends JDialog {
private boolean applySettings() {
try {
// Apply Appearance
UITheme selectedTheme = (UITheme) uiThemeBox.getSelectedItem();
if (selectedTheme != null) {
Settings.setJavaUiTheme(selectedTheme);
ThemeManager.setTheme(selectedTheme);
}
String fontFam = (String) fontBox.getSelectedItem();
if (fontFam != null) {
Settings.setFontFamily(fontFam);
@@ -668,6 +570,7 @@ public class SettingsDialog extends JDialog {
}
parentApp.getTerminalPanel().reloadSettings();
ThemeManager.applyThemeToWindow(this);
return true;
} catch (Exception e) {
@@ -26,29 +26,22 @@ public class StatusBar extends JPanel {
private Telnet3270Client client;
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() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(OIA_BG);
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
tlsStatus = createLabel("", oiaFont, OIA_FG);
luName = createLabel("", oiaFont, OIA_FG);
lockStatus = createLabel("", oiaFont, OIA_ALERT);
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
codePageInfo = createLabel("", oiaFont, OIA_DIM);
modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG);
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
lockStatus = createLabel("", oiaFont, ThemeManager.getOiaFgAlert());
fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
add(Box.createHorizontalStrut(6));
add(connectionStatus);
@@ -82,10 +75,17 @@ public class StatusBar extends JPanel {
updateStatus();
}
public void applyTheme(UITheme theme) {
setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
updateStatus();
}
public void updateStatus() {
UITheme theme = ThemeManager.getTheme();
if (client == null) {
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM);
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
tlsStatus.setText("");
luName.setText("");
lockStatus.setText("");
@@ -101,38 +101,38 @@ public class StatusBar extends JPanel {
switch (state) {
case NOT_CONNECTED:
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM);
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break;
case TCP_PENDING:
case TELNET_PENDING:
connectionStatus.setText("Connecting...");
connectionStatus.setForeground(OIA_ALERT);
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
break;
case CONNECTED_3270:
connectionStatus.setText("TN3270");
connectionStatus.setForeground(OIA_FG);
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break;
case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E");
connectionStatus.setForeground(OIA_FG);
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break;
case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(OIA_FG);
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT:
connectionStatus.setText("NVT");
connectionStatus.setForeground(OIA_FG);
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break;
case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound");
connectionStatus.setForeground(OIA_WARN);
connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
break;
default:
connectionStatus.setText(state.name());
connectionStatus.setForeground(OIA_DIM);
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break;
}
@@ -144,11 +144,11 @@ public class StatusBar extends JPanel {
String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) {
tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(OIA_FG);
tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else {
tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(new Color(255, 180, 80));
tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
}
} else {
@@ -164,6 +164,7 @@ public class StatusBar extends JPanel {
lu = "LU:" + client.getConfig().getLuName();
}
luName.setText(lu);
luName.setForeground(ThemeManager.getOiaFgNormal(theme));
// Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited();
@@ -191,10 +192,10 @@ public class StatusBar extends JPanel {
lockStatus.setText("X LOCKED");
break;
}
lockStatus.setForeground(OIA_ALERT);
lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
lockStatus.setForeground(OIA_FG);
lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
} else {
lockStatus.setText("");
}
@@ -203,10 +204,10 @@ public class StatusBar extends JPanel {
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(OIA_WARN);
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else {
fieldTypeStatus.setText("ALPHA");
fieldTypeStatus.setForeground(OIA_DIM);
fieldTypeStatus.setForeground(ThemeManager.getOiaFgDim(theme));
}
} else {
fieldTypeStatus.setText("");
@@ -215,6 +216,7 @@ public class StatusBar extends JPanel {
// Active Code Page
String cp = client.getCodePage();
codePageInfo.setText(cp != null ? "CP" + cp : "");
codePageInfo.setForeground(ThemeManager.getOiaFgDim(theme));
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
// Model & Dimensions info
@@ -222,11 +224,13 @@ public class StatusBar extends JPanel {
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
// Cursor position and buffer address
int curAddr = sb.getCursorAddress();
int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1;
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() {
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 {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor);
@@ -481,24 +491,29 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void showContextMenu(MouseEvent e) {
JPopupMenu popup = new JPopupMenu();
ThemeManager.stylePopupMenu(popup);
JMenuItem copyItem = new JMenuItem("Copy");
ThemeManager.styleMenuItem(copyItem);
copyItem.setEnabled(hasSelection());
copyItem.addActionListener(ev -> copySelection());
popup.add(copyItem);
JMenuItem pasteItem = new JMenuItem("Paste");
ThemeManager.styleMenuItem(pasteItem);
pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession());
pasteItem.addActionListener(ev -> pasteClipboard());
popup.add(pasteItem);
JMenuItem selectAllItem = new JMenuItem("Select All");
ThemeManager.styleMenuItem(selectAllItem);
selectAllItem.addActionListener(ev -> selectAll());
popup.add(selectAllItem);
popup.addSeparator();
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
ThemeManager.styleMenuItem(blockModeItem);
blockModeItem.addActionListener(ev -> {
blockSelectMode = blockModeItem.isSelected();
haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
@@ -693,19 +708,73 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
@Override
protected void processKeyEvent(KeyEvent e) {
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 (client != null) {
ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
if (client != null) {
ConnectionState state = client.getConnectionState();
if (state.isNvt()) {
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()) {
try {
client.sendNVTChar(ch);
} catch (Exception ignored) {}
e.consume();
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 (client.getNvtProcessor() != null && client.getNvtProcessor().isApplicationKeypad()) {
String kp = client.getNvtProcessor().mapKeypadKey(e.getKeyCode());
if (kp != null) {
try {
client.sendNVTString(kp);
} catch (Exception ignored) {}
e.consume();
return;
}
}
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);
refreshScreen();
e.consume();
@@ -731,7 +800,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleEnter() {
if (client != null) {
ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
if (state.isNvt()) {
try {
client.sendNVTString("\r\n");
} catch (Exception ignored) {}
@@ -744,6 +813,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleReset() {
if (client != null) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTChar('\u001B');
} catch (Exception ignored) {}
return;
}
clearSearchHighlight();
clearSelection();
client.reset();
@@ -753,6 +828,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleTab(boolean shift) {
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();
else client.tab();
refreshScreen();
@@ -761,6 +846,26 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleCursor(String dir) {
if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
int vk = KeyEvent.VK_UP;
if ("down".equals(dir)) vk = KeyEvent.VK_DOWN;
else if ("left".equals(dir)) vk = KeyEvent.VK_LEFT;
else if ("right".equals(dir)) vk = KeyEvent.VK_RIGHT;
else if ("home".equals(dir)) vk = KeyEvent.VK_HOME;
boolean handled = client.sendNVTKey(vk, '\0', false, false, false);
if (!handled) {
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) {
case "up": client.cursorUp(); break;
case "down": client.cursorDown(); break;
@@ -774,11 +879,39 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handlePF(int n) {
if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
if (client.getNvtProcessor() != null) {
client.sendNVTString(client.getNvtProcessor().getFunctionKeySequence(n));
} else {
client.sendNVTString(getNvtFunctionKeySequence(n));
}
} catch (Exception ignored) {}
return;
}
client.sendPF(n);
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) {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendPA(n);
@@ -788,6 +921,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleEraseEOF() {
if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTString("\u001B[F");
} catch (Exception ignored) {}
return;
}
client.getInputProcessor().eraseEof();
refreshScreen();
}
@@ -795,6 +934,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleDelete() {
if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTString("\u001B[3~");
} catch (Exception ignored) {}
return;
}
client.getInputProcessor().deleteChar();
refreshScreen();
}
@@ -803,7 +948,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleBackspace() {
if (client != null) {
ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
if (state.isNvt()) {
try {
client.sendNVTChar('\b');
} catch (Exception ignored) {}
@@ -824,6 +969,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleClear() {
if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTChar('\u000C');
} catch (Exception ignored) {}
return;
}
client.sendClear();
refreshScreen();
}
@@ -964,6 +1115,37 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (client != null) {
setupGraphicsPlaneRenderer();
updateCellSize();
client.setNvtClipboardHandler(new haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler() {
@Override
public String getClipboardText() {
try {
return (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor);
} catch (Exception e) {
return "";
}
}
@Override
public void setClipboardText(String text) {
if (text != null) {
try {
StringSelection ss = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
} catch (Exception ignored) {}
}
}
});
client.addNvtTitleListener(title -> {
SwingUtilities.invokeLater(() -> {
Container top = getTopLevelAncestor();
if (top instanceof JFrame) {
((JFrame) top).setTitle(title != null && !title.isEmpty() ? ("j3270 - " + title) : "j3270");
}
});
});
}
}
@@ -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) {
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
// Header
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setBackground(new Color(30, 30, 30));
headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
headerPanel.add(iconLabel);
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");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
titleLabel.setForeground(new Color(255, 180, 80));
titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel);
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.setForeground(new Color(180, 180, 180));
subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel);
headerPanel.add(titleBox);
mainPanel.add(headerPanel, BorderLayout.NORTH);
@@ -71,30 +70,27 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
detailsArea.setBackground(new Color(20, 20, 20));
detailsArea.setForeground(new Color(210, 210, 210));
ThemeManager.styleTextArea(detailsArea);
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane scrollPane = new JScrollPane(detailsArea);
scrollPane.setPreferredSize(new Dimension(520, 260));
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Buttons
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");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(new Color(220, 220, 220));
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.addActionListener(e -> {
accepted = false;
dispose();
});
JButton trustBtn = new JButton("Connect Anyway");
trustBtn.setBackground(new Color(180, 100, 40));
trustBtn.setForeground(Color.WHITE);
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
trustBtn.addActionListener(e -> {
accepted = true;
@@ -106,6 +102,7 @@ public class UntrustedCertificateDialog extends JDialog {
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
getRootPane().setDefaultButton(trustBtn);
}
@@ -0,0 +1,95 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.HeadlessException;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class MenuBarShortcutsTest {
private Map<String, JMenuItem> collectMenuItems(JMenu menu) {
Map<String, JMenuItem> map = new HashMap<>();
for (int i = 0; i < menu.getItemCount(); i++) {
JMenuItem item = menu.getItem(i);
if (item != null) {
map.put(item.getText(), item);
}
}
return map;
}
@Test
public void testActionAndViewShortcutsUseAlt() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// In automated/headless environments, JFrame cannot be initialized
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb, "JMenuBar should be present");
JMenu viewMenu = null;
JMenu actionsMenu = null;
JMenu fileMenu = null;
JMenu editMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null) {
if ("View".equals(m.getText())) viewMenu = m;
else if ("Actions".equals(m.getText())) actionsMenu = m;
else if ("File".equals(m.getText())) fileMenu = m;
else if ("Edit".equals(m.getText())) editMenu = m;
}
}
assertNotNull(viewMenu, "View menu should exist");
assertNotNull(actionsMenu, "Actions menu should exist");
assertNotNull(fileMenu, "File menu should exist");
assertNotNull(editMenu, "Edit menu should exist");
// Verify View Menu items use ALT_DOWN_MASK
Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu);
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
// Verify Actions Menu items use ALT_DOWN_MASK
Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu);
assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER);
assertAcceleratorUsesAlt(actionItems.get("Clear"), KeyEvent.VK_K);
assertAcceleratorUsesAlt(actionItems.get("Reset"), KeyEvent.VK_R);
assertAcceleratorUsesAlt(actionItems.get("Erase Input"), KeyEvent.VK_E);
assertAcceleratorUsesAlt(actionItems.get("Attention"), KeyEvent.VK_A);
assertAcceleratorUsesAlt(actionItems.get("System Request"), KeyEvent.VK_S);
assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
} finally {
// Dispose frame
app.dispose();
}
}
private void assertAcceleratorUsesAlt(JMenuItem item, int expectedKeyCode) {
assertNotNull(item, "Menu item must exist");
KeyStroke ks = item.getAccelerator();
assertNotNull(ks, "Menu item " + item.getText() + " should have an accelerator");
assertEquals(expectedKeyCode, ks.getKeyCode(), "Key code mismatch for " + item.getText());
int mods = ks.getModifiers();
assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Menu item " + item.getText() + " accelerator must have ALT modifier");
assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
"Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
}
}
@@ -14,41 +14,49 @@ public class StatusBarTest {
@Test
public void testStatusBarDoesNotContainLightPenButton() {
StatusBar statusBar = new StatusBar();
// Ensure no JButton exists in StatusBar components (lightpen button removed)
for (Component comp : statusBar.getComponents()) {
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
try {
StatusBar statusBar = new StatusBar();
// Ensure no JButton exists in StatusBar components (lightpen button removed)
for (Component comp : statusBar.getComponents()) {
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
}
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
@Test
public void testStatusBarUpdatesWithClient() {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
config.setLuName("TSU001");
config.setCodePage("1047");
Telnet3270Client client = new Telnet3270Client(config);
try {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
config.setLuName("TSU001");
config.setCodePage("1047");
Telnet3270Client client = new Telnet3270Client(config);
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
// Check labels
boolean foundCodePage = false;
boolean foundModel = false;
for (Component comp : statusBar.getComponents()) {
if (comp instanceof JLabel) {
JLabel label = (JLabel) comp;
if ("CP1047".equals(label.getText())) {
foundCodePage = true;
}
if (label.getText() != null && label.getText().contains("3279-4")) {
foundModel = true;
// Check labels
boolean foundCodePage = false;
boolean foundModel = false;
for (Component comp : statusBar.getComponents()) {
if (comp instanceof JLabel) {
JLabel label = (JLabel) comp;
if ("CP1047".equals(label.getText())) {
foundCodePage = true;
}
if (label.getText() != null && label.getText().contains("3279-4")) {
foundModel = true;
}
}
}
}
assertTrue(foundCodePage, "Should display CP1047");
assertTrue(foundModel, "Should display IBM-3279-4 model info");
assertTrue(foundCodePage, "Should display CP1047");
assertTrue(foundModel, "Should display IBM-3279-4 model info");
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
}
@@ -0,0 +1,221 @@
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() {
try {
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);
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
@Test
public void testRecursiveApplyTheme() {
try {
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());
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
@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 enum ProxyType {
NONE, HTTP, SOCKS4, SOCKS5
}
private String host;
private int port = 23;
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 String codePage = "037";
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() {}
@@ -101,6 +120,9 @@ public class ConnectionConfig {
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 void setTerminalName(String name) { this.terminalName = name; }
@@ -134,9 +156,58 @@ public class ConnectionConfig {
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"),
* 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) {
if (hostStr == null || hostStr.trim().isEmpty()) {
@@ -146,6 +217,50 @@ public class ConnectionConfig {
boolean tls = false;
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")
boolean prefixFound = true;
while (prefixFound) {
@@ -200,6 +315,9 @@ public class ConnectionConfig {
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e);
if (pType != ProxyType.NONE && pHost != null) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
return config;
}
@@ -59,4 +59,32 @@ public enum ConnectionState {
public boolean isFullSession() {
return isNvt() || is3270();
}
/**
* Map to IBM Host On-Demand ECL connection state integer codes.
* 0 = Disconnected, 1 = Connecting/Resolving, 2 = Connected (NVT/Unbound), 3 = Bound (Full 3270 session).
*/
public int toHoDStateCode() {
switch (this) {
case NOT_CONNECTED:
return 0;
case RECONNECTING:
case RESOLVING:
case TCP_PENDING:
case TLS_PENDING:
case PROXY_PENDING:
case TELNET_PENDING:
return 1;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_UNBOUND:
case CONNECTED_E_NVT:
case CONNECTED_SSCP:
return 2;
case CONNECTED_3270:
case CONNECTED_TN3270E:
default:
return 3;
}
}
}
@@ -54,6 +54,7 @@ public class Telnet3270Client {
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
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);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
@@ -62,11 +63,21 @@ public class Telnet3270Client {
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
// Wire screen update to ECLXfer for CUT mode screen tracking
// Wire screen update to ECLXfer for CUT mode screen tracking and ECLPS for event dispatching
addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
@Override public void onScreenUpdated() { xfer.onScreenUpdated(); }
@Override public void onScreenSizeChanged(int rows, int cols) {}
@Override public void onSoundAlarm() {}
@Override public void onScreenUpdated() {
xfer.onScreenUpdated();
ps.notifyPSUpdate(0, 0, screenBuffer.getRows() - 1, screenBuffer.getCols() - 1, true);
}
@Override public void onCursorMoved(int oldAddress, int newAddress) {
ps.notifyCursorMoved(oldAddress, newAddress);
}
@Override public void onScreenSizeChanged(int rows, int cols) {
ps.notifyScreenResized(rows, cols);
}
@Override public void onSoundAlarm() {
ps.notifyAlarm();
}
});
}
@@ -89,6 +100,52 @@ public class Telnet3270Client {
fsm.onConnected();
}
/**
* Connect to the configured host synchronously, blocking until the full data session is established
* (or timeout expires).
* @param timeoutMs maximum time to wait in milliseconds
* @return true if successfully connected, false if timed out
* @throws IOException if network connection fails
*/
public boolean connect(long timeoutMs) throws IOException {
connect();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
ConnectionState state = getConnectionState();
if (state.isFullSession() || (state.isFullyConnected() && isConnected())) {
return true;
}
if (state == ConnectionState.NOT_CONNECTED && !isConnected()) {
return false;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getConnectionState().isFullSession() || isConnected();
}
/**
* Connect to the configured host synchronously and block until the presentation space matches
* the specified screen descriptor.
* @param timeoutMs maximum time to wait in milliseconds
* @param desc ECLScreenDesc descriptor to match against
* @return true if successfully connected and screen matched, false otherwise
* @throws IOException if network connection fails
*/
public boolean connect(long timeoutMs, haus.nightmare.lib3270j.ecl.ECLScreenDesc desc) throws IOException {
long start = System.currentTimeMillis();
if (!connect(timeoutMs)) {
return false;
}
long elapsed = System.currentTimeMillis() - start;
long remaining = Math.max(1, timeoutMs - elapsed);
return ps.waitForScreen(desc, remaining);
}
/**
* Disconnect from the host.
*/
@@ -100,6 +157,35 @@ public class Telnet3270Client {
fsm.onDisconnect();
}
/**
* Disconnect from host with synchronous teardown.
* @param timeoutMs maximum time to wait for graceful disconnect in milliseconds
*/
public void disconnect(long timeoutMs) {
disconnect();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getConnectionState() == ConnectionState.NOT_CONNECTED && !isConnected()) {
break;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
/** HoD StopCommunication compatibility alias. */
public void stopCommunication() {
disconnect();
}
public void stopCommunication(long timeoutMs) {
disconnect(timeoutMs);
}
/**
* Check if connected (any state past TCP pending).
*/
@@ -113,11 +199,28 @@ public class Telnet3270Client {
fsm.addConnectionListener(l);
}
public void removeConnectionListener(ConnectionListener l) {
fsm.removeConnectionListener(l);
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
fsm.addScreenUpdateListener(l);
dsProcessor.addScreenUpdateListener(l);
}
public void removeScreenUpdateListener(ScreenUpdateListener l) {
fsm.removeScreenUpdateListener(l);
dsProcessor.removeScreenUpdateListener(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 ==========
/** Get the screen buffer for rendering. */
@@ -189,11 +292,48 @@ public class Telnet3270Client {
fsm.sendNVTString(s);
}
// ========== Convenience input methods ==========
/** Send an NVT key event mapped through NVT processor. */
public boolean sendNVTKey(int keyCode, char keyChar, boolean shift, boolean ctrl, boolean alt) throws IOException {
haus.nightmare.lib3270j.nvt.NvtProcessor nvt = getNvtProcessor();
if (nvt != null) {
String seq = nvt.mapKey(keyCode, keyChar, shift, ctrl, alt);
if (seq != null && !seq.isEmpty()) {
fsm.sendNvtData(seq.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
return true;
}
}
return false;
}
public void addNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
if (getNvtProcessor() != null) {
getNvtProcessor().addTitleListener(l);
}
}
public void removeNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
if (getNvtProcessor() != null) {
getNvtProcessor().removeTitleListener(l);
}
}
public void setNvtClipboardHandler(haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler h) {
if (getNvtProcessor() != null) {
getNvtProcessor().setClipboardHandler(h);
}
}
// ========== Convenience input methods & HoD compatibility ==========
/** Type a character at the cursor position. */
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. */
public void typeString(String s) {
for (char ch : s.toCharArray()) {
@@ -210,6 +350,7 @@ public class Telnet3270Client {
public void sendEnter() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
}
public void processEnter() { inputProcessor.processEnter(); }
/** Send a PF key (1-24). */
public void sendPF(int number) {
@@ -231,6 +372,7 @@ public class Telnet3270Client {
}
inputProcessor.sendAid(aid);
}
public void processPF(int number) { inputProcessor.processPF(number); }
/** Send a PA key (1-3). */
public void sendPA(int number) {
@@ -243,44 +385,104 @@ public class Telnet3270Client {
}
inputProcessor.sendAid(aid);
}
public void processPA(int number) { inputProcessor.processPA(number); }
/** Send Clear key. */
public void sendClear() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
}
public void processClear() { inputProcessor.processClear(); }
/** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); }
public void cursorUp() { inputProcessor.cursorUp(); }
public void processCursorUp() { inputProcessor.processCursorUp(); }
/** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); }
public void cursorDown() { inputProcessor.cursorDown(); }
public void processCursorDown() { inputProcessor.processCursorDown(); }
/** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); }
public void cursorLeft() { inputProcessor.cursorLeft(); }
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
/** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); }
public void cursorRight() { inputProcessor.cursorRight(); }
public void processCursorRight() { inputProcessor.processCursorRight(); }
/** 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. */
public void tab() { inputProcessor.tab(); }
public void tab() { inputProcessor.tab(); }
public void processTab() { inputProcessor.processTab(); }
/** 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. */
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. */
public void eraseInput() { inputProcessor.eraseInput(); }
public void eraseInput() { inputProcessor.eraseInput(); }
public void processEraseInput() { inputProcessor.processEraseInput(); }
/** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); }
public void processDup() { inputProcessor.processDup(); }
/** 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. */
public void attn() { inputProcessor.attn(); }
public void processAttn() { inputProcessor.processAttn(); }
/** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
public void sysReq() { inputProcessor.sysReq(); }
public void processSysReq() { inputProcessor.processSysReq(); }
/** 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. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
/** Trigger Light Pen selection at the specified screen address. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
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 processLightPen() { return inputProcessor.processLightPen(); }
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.getProgramSymbolManager();
@@ -106,6 +106,19 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
return ebcdicToString(ebcdic, offset, length, false);
}
/**
* Translate an EBCDIC byte array slice to a Unicode String with optional SO/SI escape preservation.
*
* @param ebcdic source byte array
* @param offset start offset
* @param length number of bytes
* @param preserveSOSI if true, preserves Shift-Out (\u000E) and Shift-In (\u000F) control characters
* @return translated Unicode String
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length, boolean preserveSOSI) {
if (ebcdic == null || length <= 0) return "";
StringBuilder sb = new StringBuilder(length);
@@ -117,9 +130,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int b = ebcdic[i] & 0xFF;
if (b == SO) {
inDBCS = true;
if (preserveSOSI) {
sb.append((char) SO);
}
i++;
} else if (b == SI) {
inDBCS = false;
if (preserveSOSI) {
sb.append((char) SI);
}
i++;
} else if (inDBCS) {
if (i + 1 < end) {
@@ -127,13 +146,22 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
if (b2 == SI) {
// Orphaned single byte before SI
inDBCS = false;
if (preserveSOSI) {
sb.append(ebcdicToUnicode(b));
sb.append((char) SI);
}
i += 2;
} else if (b2 == SO) {
// Unmatched byte followed by another SO
sb.append(ebcdicToUnicode(b));
i++;
} else {
sb.append(dbcsToUnicode(b, b2));
i += 2;
}
} else {
// Trailing byte
// Trailing single byte inside unclosed DBCS shift
sb.append(ebcdicToUnicode(b));
i++;
}
} else {
@@ -147,14 +175,39 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override
public byte[] stringToEbcdic(String s) {
return stringToEbcdic(s, true);
}
/**
* Translate a Unicode String to EBCDIC byte array with optional parsing of embedded SO/SI markers.
*
* @param s input Unicode string
* @param parseSOSIMarkers whether to interpret embedded \u000E (SO) and \u000F (SI) characters
* @return EBCDIC byte array with balanced SO/SI framing
*/
public byte[] stringToEbcdic(String s, boolean parseSOSIMarkers) {
if (s == null || s.isEmpty()) return new byte[0];
// Manual state machine encoding
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
boolean inDBCS = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (parseSOSIMarkers && c == (char) SO) {
if (!inDBCS) {
out.write(SO);
inDBCS = true;
}
continue;
} else if (parseSOSIMarkers && c == (char) SI) {
if (inDBCS) {
out.write(SI);
inDBCS = false;
}
continue;
}
int dbcsCode = unicodeToDbcs(c);
if (dbcsCode >= 0) {
@@ -175,10 +228,26 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
}
}
// Auto-close unclosed DBCS shift sequence
if (inDBCS) {
out.write(SI);
}
return out.toByteArray();
}
/**
* Helper to decode an EBCDIC byte array to UTF-8 String preserving SO/SI control codes.
*/
public String ebcdicToUtf8WithSOSI(byte[] ebcdic) {
if (ebcdic == null) return "";
return ebcdicToString(ebcdic, 0, ebcdic.length, true);
}
/**
* Helper to encode a UTF-8/Unicode String with embedded SO/SI markers to EBCDIC.
*/
public byte[] utf8WithSOSIToEbcdic(String s) {
return stringToEbcdic(s, true);
}
}
@@ -53,6 +53,9 @@ public class CodePageRegistry {
register(new Cp937.Cp1371());
register(new Cp933());
// Register Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS)
registerExtendedCodepages();
// Setup common aliases
addAlias("us", "037");
addAlias("usa", "037");
@@ -218,6 +221,100 @@ public class CodePageRegistry {
return CODE_PAGES.get("037");
}
/**
* Register extended EBCDIC codepages from HoD v14 converters:
* - Arabic: Cp420, Cp424
* - Hebrew: Cp424, Cp803
* - Thai: Cp838, Cp1160
* - Cyrillic: Cp1025, Cp1123, Cp1154, Cp880
* - Turkish: Cp1155, Cp905
* - Extended Chinese DBCS: Cp1388, Cp1371
*/
public static void registerExtendedCodepages() {
// Arabic & Hebrew
register(new Cp420());
register(new Cp424());
register(new Cp803());
// Thai
register(new Cp838());
register(new Cp1160());
// Cyrillic / Russian / Ukrainian
register(new Cp1025());
register(new Cp1123());
register(new Cp1154());
register(new Cp880());
// Turkish
register(new Cp1155());
register(new Cp905());
// Extended Chinese DBCS
register(new Cp1388());
register(new Cp1371());
// Setup Extended Aliases
addAlias("ar", "420");
addAlias("arabic", "420");
addAlias("ebcdic-cp-ar", "420");
addAlias("he", "424");
addAlias("hebrew", "424");
addAlias("hebrew-lowercase", "424");
addAlias("ebcdic-cp-he", "424");
addAlias("hebrew-old", "803");
addAlias("israel", "803");
addAlias("iw", "803");
addAlias("ebcdic-cp-he-old", "803");
addAlias("th", "838");
addAlias("thai", "838");
addAlias("ebcdic-cp-th", "838");
addAlias("thai-euro", "1160");
addAlias("ru", "1025");
addAlias("russian", "1025");
addAlias("cyrillic", "1025");
addAlias("ebcdic-cp-ru", "1025");
addAlias("ukraine", "1123");
addAlias("ukrainian", "1123");
addAlias("ebcdic-cp-ua", "1123");
addAlias("cyrillic-euro", "1154");
addAlias("ru-euro", "1154");
addAlias("russian-euro", "1154");
addAlias("cyrillic-russian", "880");
addAlias("ru-old", "880");
addAlias("turkish-euro", "1155");
addAlias("tr-euro", "1155");
addAlias("turkish-latin3", "905");
addAlias("tr-latin3", "905");
addAlias("chinese-ext-simplified", "1388");
addAlias("zh-simplified-ext", "1388");
addAlias("zh-ext", "1388");
addAlias("chinese-ext-traditional", "1371");
addAlias("zh-traditional-ext", "1371");
addAlias("zh-tw-ext", "1371");
}
/**
* Check if a code page is registered.
*/
public static boolean hasCodePage(String name) {
if (name == null || name.trim().isEmpty()) return false;
String raw = name.trim();
if (CODE_PAGES.containsKey(raw)) return true;
String norm = normalizeKey(raw);
if (CODE_PAGES.containsKey(norm)) return true;
return ALIASES.containsKey(norm);
}
/**
* Get an unmodifiable list of all registered built-in CodePages.
*/
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1025 (Cyrillic Multilingual EBCDIC - Russian, Bulgarian, Belarusian, Serbian, Macedonian).
* CCSID / CPGID: 1025, GCSGID: 1150.
*/
public class Cp1025 extends AbstractCodePage {
public static final String ID = "1025";
public static final String DESCRIPTION = "Cyrillic Multilingual EBCDIC";
public static final int CPGID = 1025;
public static final int GCSGID = 1150;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0453,
0x0452, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0454, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A,
// 90-9F
0x045B, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x045C, 0x045E, 0x045F, 0x0401, 0x0402, 0x0403,
// A0-AF
0x0404, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A,
// B0-BF
0x040B, 0x00A3, 0x040C, 0x00B7, 0x00A9, 0x040E, 0x040F, 0x0410,
0x0411, 0x0412, 0x00AC, 0x007C, 0x0413, 0x0414, 0x0415, 0x0416,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
};
public Cp1025() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1025", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1123 (Cyrillic Ukraine EBCDIC).
* CCSID / CPGID: 1123, GCSGID: 1399.
*/
public class Cp1123 extends AbstractCodePage {
public static final String ID = "1123";
public static final String DESCRIPTION = "Cyrillic Ukraine EBCDIC";
public static final int CPGID = 1123;
public static final int GCSGID = 1399;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0491,
0x0454, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0456, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0457, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
};
public Cp1123() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1123", MAPPING);
}
}
@@ -0,0 +1,24 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1154 (Cyrillic Multilingual with Euro \u20AC).
* CCSID / CPGID: 1154, GCSGID: 1305.
*/
public class Cp1154 extends AbstractCodePage {
public static final String ID = "1154";
public static final String DESCRIPTION = "Cyrillic Multilingual EBCDIC (with Euro \u20AC)";
public static final int CPGID = 1154;
public static final int GCSGID = 1305;
public static final int[] MAPPING = new int[256];
static {
System.arraycopy(Cp1025.MAPPING, 0, MAPPING, 0, 256);
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
}
public Cp1154() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1154", MAPPING);
}
}
@@ -0,0 +1,24 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1155 (Turkey - Turkish Latin-5 with Euro \u20AC).
* CCSID / CPGID: 1155, GCSGID: 1306.
*/
public class Cp1155 extends AbstractCodePage {
public static final String ID = "1155";
public static final String DESCRIPTION = "Turkey - Turkish Latin-5 (with Euro \u20AC)";
public static final int CPGID = 1155;
public static final int GCSGID = 1306;
public static final int[] MAPPING = new int[256];
static {
System.arraycopy(Cp1026.MAPPING, 0, MAPPING, 0, 256);
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
}
public Cp1155() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1155", MAPPING);
}
}
@@ -0,0 +1,24 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1160 (Thai EBCDIC with Euro \u20AC).
* CCSID / CPGID: 1160, GCSGID: 1176.
*/
public class Cp1160 extends AbstractCodePage {
public static final String ID = "1160";
public static final String DESCRIPTION = "Thai EBCDIC (with Euro \u20AC)";
public static final int CPGID = 1160;
public static final int GCSGID = 1176;
public static final int[] MAPPING = new int[256];
static {
System.arraycopy(Cp838.MAPPING, 0, MAPPING, 0, 256);
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
}
public Cp1160() {
super(ID, DESCRIPTION, CPGID, GCSGID, "1160", MAPPING);
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1371 (Traditional Chinese Extended Mixed DBCS).
* CCSID / CPGID: 1371, GCSGID: 1174.
*/
public class Cp1371 extends AbstractDBCSCodePage {
public static final String ID = "1371";
public static final String DESCRIPTION = "Traditional Chinese Extended Mixed DBCS";
public static final int CPGID = 1371;
public static final int GCSGID = 1174;
public Cp1371() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1371");
}
}
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1388 (Simplified Chinese Extended Mixed DBCS).
* CCSID / CPGID: 1388, GCSGID: 1175.
*/
public class Cp1388 extends AbstractDBCSCodePage {
public static final String ID = "1388";
public static final String DESCRIPTION = "Simplified Chinese Extended Mixed DBCS";
public static final int CPGID = 1388;
public static final int GCSGID = 1175;
public Cp1388() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1388");
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 420 (Arabic Bilingual EBCDIC).
* CCSID / CPGID: 420, GCSGID: 235.
*/
public class Cp420 extends AbstractCodePage {
public static final String ID = "420";
public static final String DESCRIPTION = "Arabic Bilingual EBCDIC";
public static final int CPGID = 420;
public static final int GCSGID = 235;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x0640, 0xFE83, 0xFE87, 0xFE85, 0xFE81, 0xFE80, 0x0621,
0xFE8D, 0xFE8B, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0xFE8E, 0xFE8F, 0xFE91, 0xFE93, 0xFE95, 0xFE97, 0xFE99,
0xFE9B, 0xFE9D, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0xFE9F, 0xFEA1, 0xFEA3, 0xFEA5, 0xFEA7, 0xFEA9,
0xFEAB, 0xFEAD, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0xFEAF, 0xFEB1, 0xFEB3, 0xFEB5, 0xFEB7, 0xFEB9, 0xFEBB, 0xFEBD,
0xFEBF, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0xFEC1, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0xFEC3, 0xFEC5, 0xFEC7, 0xFEC9, 0xFECB, 0xFECD,
// 90-9F
0xFECF, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0xFED1, 0xFED3, 0xFED5, 0xFED7, 0xFED9, 0xFEDB,
// A0-AF
0xFEDD, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0xFEDF, 0xFEE1, 0xFEE3, 0xFEE5, 0xFEE7, 0xFEE9,
// B0-BF
0xFEEB, 0xFEED, 0xFEEF, 0xFEF1, 0xFEF3, 0xFEF5, 0xFEF7, 0xFEF9,
0xFEFB, 0x00AE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x060C, 0x061B, 0x061F, 0x0628, 0x062A, 0x062B,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631,
// E0-EF
0x005C, 0x0632, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, 0x0638,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0639, 0x063A, 0x0641, 0x0642, 0x0643, 0x009F,
};
public Cp420() {
super(ID, DESCRIPTION, CPGID, GCSGID, "420", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 424 (Hebrew with Lowercase EBCDIC).
* CCSID / CPGID: 424, GCSGID: 941.
*/
public class Cp424 extends AbstractCodePage {
public static final String ID = "424";
public static final String DESCRIPTION = "Hebrew (with Lowercase) EBCDIC";
public static final int CPGID = 424;
public static final int GCSGID = 941;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6,
0x05D7, 0x05D8, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
0x05E0, 0x05E1, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7,
0x05E8, 0x05E9, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x05EA, 0x00A0, 0x2017, 0x00B8, 0x00A8, 0x00B4, 0x00AA, 0x00BA,
0x00DF, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp424() {
super(ID, DESCRIPTION, CPGID, GCSGID, "424", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 803 (Hebrew Old / Standard EBCDIC).
* CCSID / CPGID: 803, GCSGID: 1147.
*/
public class Cp803 extends AbstractCodePage {
public static final String ID = "803";
public static final String DESCRIPTION = "Hebrew Old / Standard EBCDIC";
public static final int CPGID = 803;
public static final int GCSGID = 1147;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6,
0x05D7, 0x05D8, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
0x05E0, 0x05E1, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7,
0x05E8, 0x05E9, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x05EA, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp803() {
super(ID, DESCRIPTION, CPGID, GCSGID, "803", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 838 (Thai EBCDIC).
* CCSID / CPGID: 838, GCSGID: 1176.
*/
public class Cp838 extends AbstractCodePage {
public static final String ID = "838";
public static final String DESCRIPTION = "Thai EBCDIC";
public static final int CPGID = 838;
public static final int GCSGID = 1176;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x0E01, 0x0E02, 0x0E03, 0x0E04, 0x0E05, 0x0E06, 0x0E07,
0x0E08, 0x0E09, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0E0A, 0x0E0B, 0x0E0C, 0x0E0D, 0x0E0E, 0x0E0F, 0x0E10,
0x0E11, 0x0E12, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x0E13, 0x0E14, 0x0E15, 0x0E16, 0x0E17, 0x0E18,
0x0E19, 0x0E1A, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0E1B, 0x0E1C, 0x0E1D, 0x0E1E, 0x0E1F, 0x0E20, 0x0E21, 0x0E22,
0x0E23, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x0E24, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0E25, 0x0E26, 0x0E27, 0x0E28, 0x0E29, 0x0E2A,
// 90-9F
0x0E2B, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x0E2C, 0x0E2D, 0x0E2E, 0x0E2F, 0x0E30, 0x0E31,
// A0-AF
0x0E32, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x0E33, 0x0E34, 0x0E35, 0x0E36, 0x0E37, 0x0E38,
// B0-BF
0x0E39, 0x0E3A, 0x0E40, 0x0E41, 0x0E42, 0x0E43, 0x0E44, 0x0E45,
0x0E46, 0x0E47, 0x00AC, 0x007C, 0x0E48, 0x0E49, 0x0E4A, 0x0E4B,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x0E4C, 0x0E4D, 0x0E4E, 0x0E4F, 0x0E50, 0x0E51,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x0E52, 0x0E53, 0x0E54, 0x0E55, 0x0E56, 0x0E57,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x0E58, 0x0E59, 0x0E5A, 0x0E5B, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp838() {
super(ID, DESCRIPTION, CPGID, GCSGID, "838", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 880 (Cyrillic Russian EBCDIC).
* CCSID / CPGID: 880, GCSGID: 960.
*/
public class Cp880 extends AbstractCodePage {
public static final String ID = "880";
public static final String DESCRIPTION = "Cyrillic Russian EBCDIC";
public static final int CPGID = 880;
public static final int GCSGID = 960;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A,
0x0449, 0x0448, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x0447, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441,
0x0440, 0x043F, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x043E, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439,
0x0438, 0x0437, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x0436, 0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451,
0x045E, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x045F, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x0456, 0x0458, 0x0459, 0x045A, 0x045C, 0x045B,
// 90-9F
0x0402, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x0403, 0x0404, 0x0405, 0x0406, 0x0408, 0x0409,
// A0-AF
0x040A, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x040C, 0x040B, 0x040F, 0x040E, 0x0401, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424,
// D0-DF
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A,
// E0-EF
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0410,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x009F,
};
public Cp880() {
super(ID, DESCRIPTION, CPGID, GCSGID, "880", MAPPING);
}
}
@@ -0,0 +1,68 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 905 (Turkey - Turkish Latin-3 EBCDIC).
* CCSID / CPGID: 905, GCSGID: 1151.
*/
public class Cp905 extends AbstractCodePage {
public static final String ID = "905";
public static final String DESCRIPTION = "Turkey - Turkish Latin-3 EBCDIC";
public static final int CPGID = 905;
public static final int GCSGID = 1151;
public static final int[] MAPPING = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x011E, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0130, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x015E, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF
0x011F, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF
0x0131, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF
0x015F, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
public Cp905() {
super(ID, DESCRIPTION, CPGID, GCSGID, "905", MAPPING);
}
}
@@ -1,10 +1,13 @@
package haus.nightmare.lib3270j.charset;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* EBCDIC ↔ Unicode character translator conforming to IBM Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
* Supports custom per-instance character translation override tables and complete
* IBM 3270 APL / Graphic Escape (GA23-0059) character mappings.
* Default: Code Page 037 (US/Canada EBCDIC).
*/
public class EbcdicTranslator {
@@ -15,6 +18,8 @@ public class EbcdicTranslator {
public static final int[] CP037_TO_UNICODE = Cp037.MAPPING;
private CodePage activeCodePage;
private final Map<Integer, Character> customEbcdicToUnicode = new ConcurrentHashMap<>();
private final Map<Character, Integer> customUnicodeToEbcdic = new ConcurrentHashMap<>();
public EbcdicTranslator() {
this("037");
@@ -85,6 +90,107 @@ public class EbcdicTranslator {
return isDBCSCodePage();
}
// =========================================================================
// Custom Character Translation Overrides
// =========================================================================
/**
* Set a bidirectional custom character translation override.
* Maps an EBCDIC byte value to a Unicode character, and vice versa.
*/
public synchronized void setCustomOverride(int ebcdicByte, char unicodeChar) {
int b = ebcdicByte & 0xFF;
customEbcdicToUnicode.put(b, unicodeChar);
customUnicodeToEbcdic.put(unicodeChar, b);
}
/**
* Set a one-way custom EBCDIC byte to Unicode character override.
*/
public synchronized void setCustomEbcdicToUnicodeOverride(int ebcdicByte, char unicodeChar) {
customEbcdicToUnicode.put(ebcdicByte & 0xFF, unicodeChar);
}
/**
* Set a one-way custom Unicode character to EBCDIC byte override.
*/
public synchronized void setCustomUnicodeToEbcdicOverride(char unicodeChar, int ebcdicByte) {
customUnicodeToEbcdic.put(unicodeChar, ebcdicByte & 0xFF);
}
/**
* Set custom translation overrides in bulk.
*/
public synchronized void setCustomOverrides(Map<Integer, Character> ebcToUni, Map<Character, Integer> uniToEbc) {
if (ebcToUni != null) {
for (Map.Entry<Integer, Character> entry : ebcToUni.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
setCustomOverride(entry.getKey(), entry.getValue());
}
}
}
if (uniToEbc != null) {
for (Map.Entry<Character, Integer> entry : uniToEbc.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
setCustomUnicodeToEbcdicOverride(entry.getKey(), entry.getValue());
}
}
}
}
/**
* Remove custom translation override for a specific EBCDIC byte.
*/
public synchronized void removeCustomOverride(int ebcdicByte) {
Character removed = customEbcdicToUnicode.remove(ebcdicByte & 0xFF);
if (removed != null) {
customUnicodeToEbcdic.remove(removed);
}
}
/**
* Remove custom translation override for a specific Unicode character.
*/
public synchronized void removeCustomUnicodeOverride(char unicodeChar) {
Integer removed = customUnicodeToEbcdic.remove(unicodeChar);
if (removed != null) {
customEbcdicToUnicode.remove(removed);
}
}
/**
* Clear all registered custom translation overrides.
*/
public synchronized void clearCustomOverrides() {
customEbcdicToUnicode.clear();
customUnicodeToEbcdic.clear();
}
/**
* Check if any custom translation overrides are active.
*/
public boolean hasCustomOverrides() {
return !customEbcdicToUnicode.isEmpty() || !customUnicodeToEbcdic.isEmpty();
}
/**
* Get an unmodifiable view of active EBCDIC-to-Unicode custom overrides.
*/
public Map<Integer, Character> getCustomEbcdicToUnicodeOverrides() {
return Collections.unmodifiableMap(new HashMap<>(customEbcdicToUnicode));
}
/**
* Get an unmodifiable view of active Unicode-to-EBCDIC custom overrides.
*/
public Map<Character, Integer> getCustomUnicodeToEbcdicOverrides() {
return Collections.unmodifiableMap(new HashMap<>(customUnicodeToEbcdic));
}
// =========================================================================
// Translation Operations
// =========================================================================
/**
* Translate double-byte EBCDIC pair (b1, b2) to Unicode.
*/
@@ -100,10 +206,15 @@ public class EbcdicTranslator {
}
/**
* Translate EBCDIC byte to Unicode character using active code page.
* Translate EBCDIC byte to Unicode character using active code page and custom overrides.
*/
public char ebcdicToUnicode(int ebc) {
return activeCodePage.ebcdicToUnicode(ebc);
int b = ebc & 0xFF;
Character custom = customEbcdicToUnicode.get(b);
if (custom != null) {
return custom;
}
return activeCodePage.ebcdicToUnicode(b);
}
/**
@@ -121,10 +232,14 @@ public class EbcdicTranslator {
}
/**
* Translate Unicode character to EBCDIC byte using active code page.
* Translate Unicode character to EBCDIC byte using active code page and custom overrides.
* Returns -1 if the character cannot be mapped.
*/
public int unicodeToEbcdic(char unicode) {
Integer custom = customUnicodeToEbcdic.get(unicode);
if (custom != null) {
return custom;
}
return activeCodePage.unicodeToEbcdic(unicode);
}
@@ -132,20 +247,37 @@ public class EbcdicTranslator {
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/
public byte unicodeToEbcdicSafe(char unicode) {
return activeCodePage.unicodeToEbcdicSafe(unicode);
int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
}
/**
* Translate a byte array from EBCDIC to a Unicode string using active code page.
* Translate a byte array from EBCDIC to a Unicode string using active code page and overrides.
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
if (ebcdic == null || length <= 0) return "";
if (hasCustomOverrides() || !(activeCodePage instanceof AbstractDBCSCodePage)) {
char[] chars = new char[length];
for (int i = 0; i < length; i++) {
chars[i] = ebcdicToUnicode(ebcdic[offset + i]);
}
return new String(chars);
}
return activeCodePage.ebcdicToString(ebcdic, offset, length);
}
/**
* Translate a Unicode string to EBCDIC byte array using active code page.
* Translate a Unicode string to EBCDIC byte array using active code page and overrides.
*/
public byte[] stringToEbcdic(String s) {
if (s == null || s.isEmpty()) return new byte[0];
if (hasCustomOverrides() || !(activeCodePage instanceof AbstractDBCSCodePage)) {
byte[] bytes = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
bytes[i] = unicodeToEbcdicSafe(s.charAt(i));
}
return bytes;
}
return activeCodePage.stringToEbcdic(s);
}
@@ -164,11 +296,12 @@ 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) {
switch (ec & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
public char mapAPL(int ebcdicCodePoint) {
switch (ebcdicCodePoint & 0xFF) {
// Standard Box-Drawing Lines, Corners, T-Junctions, and Crosses
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
@@ -181,7 +314,7 @@ public class EbcdicTranslator {
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
// Mathematical Relations and Punctuation
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
@@ -193,12 +326,66 @@ public class EbcdicTranslator {
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xAF: return '\u00AF'; // Overbar / High Minus '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return ebcdicToUnicode(ec & 0xFF);
// IBM 3270 APL Operational & Structural Glyphs
case 0x80: return '\u22C4'; // Diamond '⋄'
case 0x81: return '\u237A'; // APL Alpha ''
case 0x82: return '\u22A5'; // Up Tack / Decode '⊥'
case 0x83: return '\u2229'; // Intersection '∩'
case 0x84: return '\u230A'; // Floor '⌊'
case 0x86: return '\u2286'; // Subset or equal '⊆'
case 0x87: return '\u2207'; // Del / Grad '∇'
case 0x88: return '\u2206'; // Delta '∆'
case 0x89: return '\u2373'; // Iota ''
case 0x8A: return '\u2192'; // Right Arrow '→'
case 0x8B: return '\u235E'; // Quote Quad '⍞'
case 0x8E: return '\u00D7'; // Multiply '×'
case 0x8F: return '\u00F7'; // Divide '÷'
case 0x90: return '\u235F'; // Circle Star / Log '⍟'
case 0x91: return '\u2339'; // Quad Divide / Domino '⌹'
case 0x92: return '\u22A4'; // Down Tack / Encode ''
case 0x93: return '\u222A'; // Union ''
case 0x94: return '\u2308'; // Ceiling '⌈'
case 0x95: return '\u2374'; // Rho / Shape ''
case 0x96: return '\u2375'; // APL Omega '⍵'
case 0x97: return '\u2260'; // Not equal '≠'
case 0x98: return '\u2377'; // Epsilon Underbar '⍷'
case 0x99: return '\u25CB'; // Circle '○'
case 0x9A: return '\u2190'; // Left Arrow '←'
case 0x9B: return '\u2359'; // Delta Underbar '⍙'
case 0x9C: return '\u234B'; // Grade Up '⍋'
case 0x9E: return '\u2352'; // Grade Down '⍒'
case 0x9F: return '\u235D'; // Lamp / Comment '⍝'
case 0xA1: return '\u00A8'; // Diaeresis '¨'
case 0xA4: return '\u2336'; // I-Beam '⌶'
case 0xA5: return '\u2355'; // Thorn / Format '⍕'
case 0xA6: return '\u2282'; // Left Shoe / Enclose '⊂'
case 0xA7: return '\u2283'; // Right Shoe / Disclose '⊃'
case 0xA8: return '\u2191'; // Up Arrow / Take '↑'
case 0xA9: return '\u2193'; // Down Arrow / Drop '↓'
case 0xAA: return '\u2395'; // Quad '⎕'
case 0xAB: return '\u234E'; // Execute / Hydra '⍎'
case 0xAC: return '\u2349'; // Transpose / Circle Slope '⍉'
case 0xB4: return '\u2296'; // Circle Bar / Reverse '⊖'
case 0xB5: return '\u236A'; // Comma Bar / Table '⍪'
case 0xB6: return '\u236B'; // Del Tilde '⍫'
case 0xB7: return '\u236C'; // Zilde '⍬'
case 0xB9: return '\u233F'; // Slash Bar '⌿'
case 0xBB: return '\u2340'; // Backslash Bar '⍀'
case 0xBC: return '\u2338'; // Quad Equal '⌸'
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);
}
}
@@ -109,6 +109,10 @@ public class DataStreamProcessor {
screenListeners.add(l);
}
public void removeScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.remove(l);
}
/**
* Process a 3270 data stream record.
*
@@ -144,6 +148,9 @@ public class DataStreamProcessor {
int oldCols = screen.getCols();
screen.erase(false);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
@@ -161,6 +168,9 @@ public class DataStreamProcessor {
int oldCols = screen.getCols();
screen.erase(true);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
@@ -510,11 +520,14 @@ public class DataStreamProcessor {
// Handle GE (graphic escape) prefix
byte fillCs = currentCs;
char fillUcs4 = 0;
if (pos + 4 < end && fillChar == ORDER_GE) {
fillChar = data[pos + 4] & 0xFF;
fillCs = CS_GE;
fillUcs4 = (translator != null) ? translator.mapAPL(fillChar) : (char) fillChar;
pos += 5;
} else {
fillUcs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar) : (char) fillChar;
pos += 4;
}
@@ -523,6 +536,7 @@ public class DataStreamProcessor {
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar;
ea.ucs4 = fillUcs4;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
@@ -575,6 +589,7 @@ public class DataStreamProcessor {
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) geChar;
ea.cs = CS_GE;
ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
@@ -680,7 +695,7 @@ public class DataStreamProcessor {
// ========== Read Buffer ==========
private void processReadBuffer() {
public void processReadBuffer() {
outputPos = 0;
int size = screen.getRows() * screen.getCols();
@@ -780,11 +795,21 @@ public class DataStreamProcessor {
// ========== Read Modified ==========
private void processReadModified(boolean all) {
public void processReadModified(boolean all) {
if (ftDft != null && ftDft.readModified()) {
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;
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
@@ -843,10 +868,11 @@ public class DataStreamProcessor {
sendOutput();
}
// ========== Write Structured Field ==========
private void processWriteStructuredField(byte[] data, int offset, int length) {
int pos = offset + 1; // Skip WSF command byte
public void processWriteStructuredField(byte[] data, int offset, int length) {
if (data == null || length <= 0)
return;
int firstByte = data[offset] & 0xFF;
int pos = (firstByte == CMD_WSF || firstByte == SNA_CMD_WSF) ? offset + 1 : offset;
int end = offset + length;
while (pos < end) {
@@ -881,10 +907,19 @@ public class DataStreamProcessor {
case SF_ACTIVATE_PART:
processActivatePartition(data, pos, fieldLen);
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);
break;
case SF_TRANSFER_DATA:
case SF_TRANSFER_DATA: // 0xD0
if (ftDft != null) {
ftDft.processStructuredField(data, pos, fieldLen);
} else {
@@ -898,95 +933,7 @@ public class DataStreamProcessor {
programSymbolManager.loadps(psData);
}
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_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
@@ -1274,11 +1221,275 @@ public class DataStreamProcessor {
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) {
if (fieldLen > 5) {
if (fieldLen >= 4) {
int pid = data[offset + 3] & 0xFF;
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);
}
private byte[] buildSummary() {
public byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
@@ -307,7 +307,13 @@ public class QueryReplyBuilder {
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);
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
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 & 0xFF); // usable height low
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 >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 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 >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF);
@@ -359,8 +365,14 @@ public class QueryReplyBuilder {
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
}
private byte[] buildAlphaPartitions(int maxRows) {
int bufSize = screen.getMaxCols() * screen.getMaxRows();
public byte[] buildAlphaPartitions() {
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);
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high
@@ -369,7 +381,7 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
private byte[] buildCharsets() {
public byte[] buildCharsets() {
int charW = getCharWidth();
int charH = getCharHeight();
@@ -429,7 +441,7 @@ public class QueryReplyBuilder {
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)
return new byte[] {
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)
return new byte[] {
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 };
}
private byte[] buildDdm(int bufferSize) {
public byte[] buildDdm() {
return buildDdm(4096);
}
public byte[] buildDdm(int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved
out.write(0x00); // reserved
@@ -470,7 +486,13 @@ public class QueryReplyBuilder {
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);
// 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 };
}
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) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
return new byte[]{
@@ -524,10 +552,20 @@ public class QueryReplyBuilder {
};
}
public byte[] buildGraphics() {
return buildSegment();
}
public byte[] buildGraphics(int maxCols, int 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) {
// 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[]{
@@ -540,6 +578,10 @@ public class QueryReplyBuilder {
};
}
public byte[] buildGImage() {
return buildProcedure();
}
public byte[] buildGImage(int maxCols, int maxRows) {
return buildProcedure(maxCols, maxRows);
}
@@ -581,6 +623,16 @@ public class QueryReplyBuilder {
appendPort(out);
}
public byte[] buildPort() {
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
appendPort(out);
return out.toByteArray();
}
public byte[] buildOemFormat() {
return buildPort();
}
public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
@@ -602,6 +654,10 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
public byte[] buildGraphicColor() {
return buildGrColor();
}
public byte[] buildGColor() {
return buildGrColor();
}
@@ -0,0 +1,13 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.Telnet3270Client;
/**
* IBM Host On-Demand ECLConnection drop-in compatibility class.
*/
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection {
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session, Telnet3270Client client) {
super(session, client);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLConstants compatibility interface.
*/
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLErrors compatibility interface.
*/
public interface ECLErrors extends haus.nightmare.lib3270j.ecl.ECLErrors {
}
@@ -0,0 +1,21 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLException compatibility class.
*/
public class ECLException extends haus.nightmare.lib3270j.ecl.ECLException {
private static final long serialVersionUID = 1L;
public ECLException(int errorCode, String message) {
super(errorCode, message);
}
public ECLException(int errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
public ECLException(String message) {
super(message);
}
}
@@ -0,0 +1,11 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLField drop-in compatibility class.
*/
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField {
public ECLField(haus.nightmare.lib3270j.ecl.ECLPS ps, int startPos, int dataStart, int endPos, int length, byte attribute) {
super(ps, startPos, dataStart, endPos, length, attribute);
}
}
@@ -0,0 +1,13 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* IBM Host On-Demand ECLFieldList drop-in compatibility class.
*/
public class ECLFieldList extends haus.nightmare.lib3270j.ecl.ECLFieldList {
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLPS ps, ScreenBuffer screen) {
super(ps, screen);
}
}
@@ -0,0 +1,15 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
/**
* IBM Host On-Demand ECLOIA drop-in compatibility class.
*/
public class ECLOIA extends haus.nightmare.lib3270j.ecl.ECLOIA {
public ECLOIA(ScreenBuffer screen, InputProcessor inputProcessor, TelnetFSM fsm) {
super(screen, inputProcessor, fsm);
}
}
@@ -0,0 +1,15 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* IBM Host On-Demand ECLPS drop-in compatibility class.
*/
public class ECLPS extends haus.nightmare.lib3270j.ecl.ECLPS {
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
super(screen, inputProcessor, translator);
}
}
@@ -0,0 +1,11 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
/**
* IBM Host On-Demand ECLScreenDesc drop-in compatibility class.
*/
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc {
public ECLScreenDesc() {
super();
}
}
@@ -0,0 +1,37 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import java.util.Properties;
/**
* IBM Host On-Demand ECLSession drop-in compatibility class.
*/
public class ECLSession extends haus.nightmare.lib3270j.ecl.ECLSession {
public ECLSession() {
super();
}
public ECLSession(ConnectionConfig config) {
super(config);
}
public ECLSession(String host, int port, TerminalModel model) {
super(host, port, model);
}
public ECLSession(String host, int port, TerminalModel model, boolean useTls) {
super(host, port, model, useTls);
}
public ECLSession(Properties props) {
super(props);
}
public ECLSession(Telnet3270Client client) {
super(client);
}
}
@@ -0,0 +1,16 @@
package haus.nightmare.lib3270j.eNetwork.ECL;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* IBM Host On-Demand ECLXfer drop-in compatibility class.
*/
public class ECLXfer extends haus.nightmare.lib3270j.ecl.ECLXfer {
public ECLXfer(ScreenBuffer screen, InputProcessor input, DataStreamProcessor dsProcessor, EbcdicTranslator translator) {
super(screen, input, dsProcessor, translator);
}
}
@@ -0,0 +1,16 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
import haus.nightmare.lib3270j.ConnectionState;
/**
* IBM Host On-Demand ECLCommEvent compatibility class.
*/
public class ECLCommEvent extends haus.nightmare.lib3270j.ecl.ECLCommEvent {
private static final long serialVersionUID = 1L;
public ECLCommEvent(Object source, int eventType, ConnectionState oldState, ConnectionState newState,
String message, String deviceType, String deviceName) {
super(source, eventType, oldState, newState, message, deviceType, deviceName);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLCommListener compatibility interface.
*/
public interface ECLCommListener extends haus.nightmare.lib3270j.ecl.ECLCommListener {
}
@@ -0,0 +1,8 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLCommNotify compatibility interface.
*/
@FunctionalInterface
public interface ECLCommNotify extends haus.nightmare.lib3270j.ecl.ECLCommNotify {
}
@@ -0,0 +1,14 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLOIAEvent compatibility class.
*/
public class ECLOIAEvent extends haus.nightmare.lib3270j.ecl.ECLOIAEvent {
private static final long serialVersionUID = 1L;
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source, eventType, inputInhibited, alphanumericType, insertMode, statusString);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLOIAListener compatibility interface.
*/
public interface ECLOIAListener extends haus.nightmare.lib3270j.ecl.ECLOIAListener {
}
@@ -0,0 +1,18 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLPSEvent compatibility class.
*/
public class ECLPSEvent extends haus.nightmare.lib3270j.ecl.ECLPSEvent {
private static final long serialVersionUID = 1L;
public ECLPSEvent(Object source, int eventType, int startRow, int startCol, int endRow, int endCol,
int oldCursorAddress, int newCursorAddress, int rows, int cols, boolean fullUpdate) {
super(source, eventType, startRow, startCol, endRow, endCol, oldCursorAddress, newCursorAddress, rows, cols, fullUpdate);
}
public ECLPSEvent(Object source, int eventType) {
super(source, eventType);
}
}
@@ -0,0 +1,7 @@
package haus.nightmare.lib3270j.eNetwork.ECL.event;
/**
* IBM Host On-Demand ECLPSListener compatibility interface.
*/
public interface ECLPSListener extends haus.nightmare.lib3270j.ecl.ECLPSListener {
}
@@ -0,0 +1,73 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionState;
import java.util.EventObject;
/**
* Event object dispatched on communication lifecycle and state transitions.
*/
public class ECLCommEvent extends EventObject {
private static final long serialVersionUID = 1L;
public static final int COMM_CONNECTING = 1;
public static final int COMM_CONNECTED = 2;
public static final int COMM_DISCONNECTED = 3;
public static final int COMM_STATE_CHANGED = 4;
public static final int COMM_ERROR = 5;
public static final int COMM_UNBOUND = 6;
public static final int COMM_BIND = 7;
public static final int EVENT_CONNECTING = COMM_CONNECTING;
public static final int EVENT_CONNECTED = COMM_CONNECTED;
public static final int EVENT_DISCONNECTED = COMM_DISCONNECTED;
public static final int EVENT_STATE_CHANGED = COMM_STATE_CHANGED;
public static final int EVENT_ERROR = COMM_ERROR;
public static final int EVENT_UNBOUND = COMM_UNBOUND;
public static final int EVENT_BIND = COMM_BIND;
private final int eventType;
private final ConnectionState oldState;
private final ConnectionState newState;
private final String message;
private final String deviceType;
private final String deviceName;
public ECLCommEvent(Object source, int eventType, ConnectionState oldState, ConnectionState newState,
String message, String deviceType, String deviceName) {
super(source);
this.eventType = eventType;
this.oldState = oldState;
this.newState = newState;
this.message = message;
this.deviceType = deviceType;
this.deviceName = deviceName;
}
public int getEventType() { return eventType; }
public ConnectionState getOldState() { return oldState; }
public ConnectionState getNewState() { return newState; }
public String getMessage() { return message; }
public String getErrorMessage() { return message; }
public String getDeviceType() { return deviceType; }
public String getDeviceName() { return deviceName; }
public String getLUName() { return deviceName; }
public boolean isConnected() {
return newState != null && newState.isConnected();
}
public boolean isFullSession() {
return newState != null && newState.isFullSession();
}
public ECLConnection getConnection() {
return (getSource() instanceof ECLConnection) ? (ECLConnection) getSource() : null;
}
@Override
public String toString() {
return String.format("ECLCommEvent[type=%d, oldState=%s, newState=%s, msg='%s', dev='%s', lu='%s']",
eventType, oldState, newState, message, deviceType, deviceName);
}
}
@@ -0,0 +1,31 @@
package haus.nightmare.lib3270j.ecl;
/**
* Listener interface for communication lifecycle events.
*/
public interface ECLCommListener {
/**
* Called when the communication connection state or status changes.
* @param event ECLCommEvent containing transition details
*/
void commEvent(ECLCommEvent event);
/**
* Called specifically when the connection is established.
* @param event ECLCommEvent
*/
default void commConnected(ECLCommEvent event) {}
/**
* Called specifically when the connection is disconnected.
* @param event ECLCommEvent
*/
default void commDisconnected(ECLCommEvent event) {}
/**
* Called specifically when a communication error occurs.
* @param event ECLCommEvent
*/
default void commError(ECLCommEvent event) {}
}
@@ -0,0 +1,14 @@
package haus.nightmare.lib3270j.ecl;
/**
* IBM Host On-Demand ECLCommNotify callback interface.
*/
@FunctionalInterface
public interface ECLCommNotify {
/**
* Notification callback invoked on connection state change.
* @param connected true if session is connected, false otherwise
*/
void CommNotify(boolean connected);
}
@@ -0,0 +1,230 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.listener.ConnectionListener;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Communication and session connection state management conforming to IBM Host On-Demand ECL.
*/
public class ECLConnection {
private final ECLSession session;
private final Telnet3270Client client;
private final List<ECLCommListener> commListeners = new CopyOnWriteArrayList<>();
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
public ECLConnection(ECLSession session, Telnet3270Client client) {
this.session = session;
this.client = client;
if (client != null) {
client.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
int eventType = ECLCommEvent.COMM_STATE_CHANGED;
if (newState.isFullSession()) {
eventType = ECLCommEvent.COMM_CONNECTED;
} else if (newState == ConnectionState.NOT_CONNECTED) {
eventType = ECLCommEvent.COMM_DISCONNECTED;
} else if (newState.isHalfConnected()) {
eventType = ECLCommEvent.COMM_CONNECTING;
}
String devType = (client.getTelnetFSM() != null) ? client.getTelnetFSM().getConnectedType() : null;
String lu = (client.getTelnetFSM() != null) ? client.getTelnetFSM().getConnectedLu() : null;
ECLCommEvent event = new ECLCommEvent(ECLConnection.this, eventType, oldState, newState,
"State: " + newState, devType, lu);
notifyCommEvent(event);
}
@Override
public void onConnectionError(String message) {
ConnectionState state = client.getConnectionState();
ECLCommEvent event = new ECLCommEvent(ECLConnection.this, ECLCommEvent.COMM_ERROR,
state, state, message, null, null);
notifyCommEvent(event);
}
@Override
public void onTN3270ENegotiated(String deviceType, String deviceName) {
ConnectionState state = client.getConnectionState();
ECLCommEvent event = new ECLCommEvent(ECLConnection.this, ECLCommEvent.COMM_BIND,
state, state, "TN3270E Negotiated", deviceType, deviceName);
notifyCommEvent(event);
}
});
}
}
public ECLSession GetSession() { return session; }
public ECLSession getSession() { return session; }
public Telnet3270Client GetClient() { return client; }
public Telnet3270Client getClient() { return client; }
public String GetHost() {
return (client != null && client.getConfig() != null) ? client.getConfig().getHost() : "";
}
public String getHost() { return GetHost(); }
public int GetPort() {
return (client != null && client.getConfig() != null) ? client.getConfig().getPort() : 23;
}
public int getPort() { return GetPort(); }
public String GetCodePage() {
return (client != null) ? client.getCodePage() : "037";
}
public String getCodePage() { return GetCodePage(); }
public TerminalModel GetModel() {
return (client != null && client.getConfig() != null) ? client.getConfig().getModel() : TerminalModel.IBM_3279_4;
}
public TerminalModel getModel() { return GetModel(); }
public String GetLUName() {
if (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) {
return client.getTelnetFSM().getConnectedLu();
}
return (client != null && client.getConfig() != null) ? client.getConfig().getLuName() : null;
}
public String getLUName() { return GetLUName(); }
public String GetDevName() { return GetLUName(); }
public String getDevName() { return GetLUName(); }
public String GetConnType() {
if (client == null) return "UNKNOWN";
ConnectionState cs = client.getConnectionState();
if (cs.isTn3270e()) return "TN3270E";
if (cs.is3270()) return "TN3270";
if (cs.isNvt()) return "NVT";
if (client.getConfig() != null) {
return client.getConfig().isTn3270eEnabled() ? "TN3270E" : "TN3270";
}
return "UNKNOWN";
}
public String getConnType() { return GetConnType(); }
public ConnectionState GetState() {
return (client != null) ? client.getConnectionState() : ConnectionState.NOT_CONNECTED;
}
public ConnectionState getState() { return GetState(); }
public int GetStateCode() {
return GetState().toHoDStateCode();
}
public int getStateCode() { return GetStateCode(); }
public boolean IsConnected() {
return client != null && client.isConnected();
}
public boolean isConnected() { return IsConnected(); }
public boolean IsStarted() {
return IsConnected();
}
public boolean isStarted() { return IsStarted(); }
public boolean IsReady() {
return client != null && client.getConnectionState().isFullSession();
}
public boolean isReady() { return IsReady(); }
public boolean IsConnecting() {
return client != null && client.getConnectionState().isHalfConnected();
}
public boolean isConnecting() { return IsConnecting(); }
public boolean IsDisconnecting() {
return client == null || client.getConnectionState() == ConnectionState.NOT_CONNECTED;
}
public boolean isDisconnecting() { return IsDisconnecting(); }
public boolean IsSSL() {
return client != null && client.getConfig() != null && client.getConfig().isUseTls();
}
public boolean isSSL() { return IsSSL(); }
public boolean IsTLS() { return IsSSL(); }
public boolean isTLS() { return IsSSL(); }
public void Connect() throws IOException {
if (client != null) {
client.connect();
}
}
public void connect() throws IOException { Connect(); }
public void Disconnect() {
if (client != null) {
client.disconnect();
}
}
public void disconnect() { Disconnect(); }
public void StartCommunication() throws IOException { Connect(); }
public void startCommunication() throws IOException { Connect(); }
public void StopCommunication() { Disconnect(); }
public void stopCommunication() { Disconnect(); }
// ========== Event Listener Management ==========
public void RegisterCommEvent(ECLCommListener listener) {
if (listener != null && !commListeners.contains(listener)) {
commListeners.add(listener);
}
}
public void registerCommEvent(ECLCommListener listener) { RegisterCommEvent(listener); }
public void UnregisterCommEvent(ECLCommListener listener) {
commListeners.remove(listener);
}
public void unregisterCommEvent(ECLCommListener listener) { UnregisterCommEvent(listener); }
public void RegisterCommEvent(ECLCommNotify notify, boolean sync) {
if (notify != null && !commNotifies.contains(notify)) {
commNotifies.add(notify);
}
}
public void registerCommEvent(ECLCommNotify notify, boolean sync) { RegisterCommEvent(notify, sync); }
public void UnregisterCommEvent(ECLCommNotify notify) {
commNotifies.remove(notify);
}
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
private void notifyCommEvent(ECLCommEvent event) {
for (ECLCommListener l : commListeners) {
try {
l.commEvent(event);
if (event.getEventType() == ECLCommEvent.COMM_CONNECTED) {
l.commConnected(event);
} else if (event.getEventType() == ECLCommEvent.COMM_DISCONNECTED) {
l.commDisconnected(event);
} else if (event.getEventType() == ECLCommEvent.COMM_ERROR) {
l.commError(event);
}
} catch (Exception ignored) {}
}
for (ECLCommNotify n : commNotifies) {
try {
n.CommNotify(event.isConnected());
} catch (Exception ignored) {}
}
}
@Override
public String toString() {
return String.format("ECLConnection[host=%s, port=%d, state=%s, lu=%s, ssl=%b]",
GetHost(), GetPort(), GetState(), GetLUName(), IsSSL());
}
}
@@ -40,6 +40,8 @@ public interface ECLConstants {
// Search directions
int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2;
int DIR_FORWARD = 1;
int DIR_BACKWARD = 2;
// OIA Input Inhibited Reason Codes
int INHIBIT_NOT_INHIBITED = 0;
@@ -49,4 +51,23 @@ public interface ECLConstants {
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
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;
}
@@ -0,0 +1,21 @@
package haus.nightmare.lib3270j.ecl;
/**
* Common error codes for IBM Host On-Demand Emulator Class Library (ECL) emulation.
*/
public interface ECLErrors {
int ECL_ERR_NONE = 0;
int ECL_ERR_COMM_NOT_CONNECTED = 1;
int ECL_ERR_COMM_TIMEOUT = 2;
int ECL_ERR_COMM_FAILED = 3;
int ECL_ERR_SCREEN_MATCH_TIMEOUT = 4;
int ECL_ERR_PS_NOT_AVAILABLE = 5;
int ECL_ERR_OIA_NOT_AVAILABLE = 6;
int ECL_ERR_INVALID_PARAM = 7;
int ECL_ERR_INVALID_POSITION = 8;
int ECL_ERR_FIELD_NOT_FOUND = 9;
int ECL_ERR_KEYBOARD_LOCKED = 10;
int ECL_ERR_XFER_FAILED = 11;
int ECL_ERR_SESSION_ALREADY_OPEN = 12;
int ECL_ERR_SESSION_CLOSED = 13;
}
@@ -0,0 +1,33 @@
package haus.nightmare.lib3270j.ecl;
/**
* Exception class for IBM Host On-Demand ECL operations.
*/
public class ECLException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int errorCode;
public ECLException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ECLException(int errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public ECLException(String message) {
this(ECLErrors.ECL_ERR_COMM_FAILED, message);
}
public int getErrorCode() {
return errorCode;
}
@Override
public String toString() {
return "ECLException[errorCode=" + errorCode + ", message=" + getMessage() + "]";
}
}
@@ -1,5 +1,7 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
@@ -26,67 +28,93 @@ public class ECLField {
/** Buffer address of the field attribute character. */
public int getStart() { return startPos; }
public int GetStart() { return getStart(); }
/** First buffer address of the field data (start + 1). */
public int getDataStart() { return dataStart; }
public int GetDataStart() { return getDataStart(); }
/** Last buffer address of the field data inclusive. */
public int getEnd() { return endPos; }
public int GetEnd() { return getEnd(); }
/** Number of data characters in the field. */
public int getLength() { return length; }
public int GetLength() { return getLength(); }
public int getStartRow() {
int cols = ps.getCols();
return cols > 0 ? startPos / cols : 0;
}
public int GetStartRow() { return getStartRow(); }
public int getStartCol() {
int cols = ps.getCols();
return cols > 0 ? startPos % cols : 0;
}
public int GetStartCol() { return getStartCol(); }
public int getEndRow() {
int cols = ps.getCols();
return cols > 0 ? endPos / cols : 0;
}
public int GetEndRow() { return getEndRow(); }
public int getEndCol() {
int cols = ps.getCols();
return cols > 0 ? endPos % cols : 0;
}
public int GetEndCol() { return getEndCol(); }
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() {
return faIsModified(attribute & 0xFF);
return faIsModified(getLiveAttribute() & 0xFF);
}
public boolean IsModified() { return isModified(); }
public boolean isProtected() {
return faIsProtected(attribute & 0xFF);
return faIsProtected(getLiveAttribute() & 0xFF);
}
public boolean IsProtected() { return isProtected(); }
public boolean isNumeric() {
return faIsNumeric(attribute & 0xFF);
return faIsNumeric(getLiveAttribute() & 0xFF);
}
public boolean IsNumeric() { return isNumeric(); }
public boolean isHighIntensity() {
return faIsHigh(attribute & 0xFF);
return faIsHigh(getLiveAttribute() & 0xFF);
}
public boolean IsHighIntensity() { return isHighIntensity(); }
public boolean isHidden() {
return faIsZero(attribute & 0xFF);
return faIsZero(getLiveAttribute() & 0xFF);
}
public boolean IsHidden() { return isHidden(); }
public boolean isDisplay() {
return !isHidden();
}
public boolean IsDisplay() { return isDisplay(); }
public boolean isPenSelectable() {
return faIsSelectable(attribute & 0xFF);
return faIsSelectable(getLiveAttribute() & 0xFF);
}
public boolean IsPenSelectable() { return isPenSelectable(); }
public short getAttribute() {
return (short) (attribute & 0xFF);
return (short) (getLiveAttribute() & 0xFF);
}
public short GetAttribute() { return getAttribute(); }
/**
* Get the text contents of this field as a String.
@@ -95,6 +123,7 @@ public class ECLField {
if (length <= 0) return "";
return ps.getString(dataStart, length);
}
public String GetText() { return getText(); }
/**
* Set the text contents of this field.
@@ -103,6 +132,7 @@ public class ECLField {
if (isProtected() || length <= 0) return;
ps.setText(text, dataStart);
}
public void SetText(String text) { setText(text); }
/**
* Get selector light pen type.
@@ -117,6 +147,7 @@ public class ECLField {
}
return ' ';
}
public char GetSelectorPenType() { return getSelectorPenType(); }
/**
* Actuate lightpen selection on this field ('?' -> '>').
@@ -128,6 +159,7 @@ public class ECLField {
setText(">" + (t.length() > 1 ? t.substring(1) : ""));
}
}
public void SelectField() { selectField(); }
/**
* Deselect lightpen selection on this field ('>' -> '?').
@@ -139,6 +171,100 @@ public class ECLField {
setText("?" + (t.length() > 1 ? t.substring(1) : ""));
}
}
public void DeSelectField() { deSelectField(); }
/** Return true if this field wraps from bottom of screen to top. */
public boolean isWrapped() {
return startPos > endPos;
}
public boolean IsWrapped() { return isWrapped(); }
/** Last data buffer address (same as getEnd). */
public int getDataEnd() {
return endPos;
}
public int GetDataEnd() { return getDataEnd(); }
/**
* 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;
}
}
public boolean Contains(int pos) { return contains(pos); }
/**
* 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);
}
public boolean Contains(int row, int col) { return contains(row, 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();
}
}
}
public void SetModified(boolean modified) { setModified(modified); }
/**
* 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();
}
public void Erase() { erase(); }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ECLField)) return false;
ECLField other = (ECLField) o;
return this.startPos == other.startPos && this.endPos == other.endPos && this.length == other.length;
}
@Override
public int hashCode() {
return java.util.Objects.hash(startPos, endPos, length);
}
@Override
public String toString() {
@@ -70,27 +70,38 @@ public class ECLFieldList {
}
}
public void Refresh() { refresh(); }
public synchronized int getFieldCount() {
return fields.size();
}
public int GetFieldCount() { return getFieldCount(); }
public synchronized List<ECLField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields));
}
public List<ECLField> GetFields() { return getFields(); }
public synchronized ECLField getFirstField() {
if (fields.isEmpty()) return null;
return fields.get(0);
}
public ECLField GetFirstField() { return getFirstField(); }
public synchronized ECLField getNextField(ECLField prev) {
if (prev == null || fields.isEmpty()) return getFirstField();
int idx = fields.indexOf(prev);
if (idx >= 0 && idx + 1 < fields.size()) {
return fields.get(idx + 1);
for (int i = 0; i < fields.size(); i++) {
ECLField f = fields.get(i);
if (f.equals(prev) || f.getStart() == prev.getStart()) {
if (i + 1 < fields.size()) {
return fields.get(i + 1);
}
return null;
}
}
return null;
}
public ECLField GetNextField(ECLField prev) { return getNextField(prev); }
/**
* Find the field that contains the specified buffer position.
@@ -113,6 +124,7 @@ public class ECLFieldList {
}
return null;
}
public ECLField FindField(int pos) { return findField(pos); }
/**
* Find the field at the specified 0-indexed row and column.
@@ -122,6 +134,62 @@ public class ECLFieldList {
int cols = screen.getCols();
return findField(row * cols + col);
}
public ECLField FindField(int row, int col) { return findField(row, 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;
for (int i = 0; i < fields.size(); i++) {
ECLField f = fields.get(i);
if (f.equals(next) || f.getStart() == next.getStart()) {
if (i > 0) {
return fields.get(i - 1);
} else {
return fields.get(fields.size() - 1);
}
}
}
return null;
}
public ECLField GetPreviousField(ECLField next) { return getPreviousField(next); }
/**
* Find the field at the given buffer position (alias for findField).
*/
public ECLField findFieldAt(int pos) {
return findField(pos);
}
public ECLField FindFieldAt(int pos) { return findFieldAt(pos); }
/**
* Find the field at the given row and column.
*/
public ECLField findFieldAt(int row, int col) {
return findField(row, col);
}
public ECLField FindFieldAt(int row, int col) { return findFieldAt(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);
}
public ECLField FindPrevField(int pos) { return findPrevField(pos); }
/**
* 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);
}
public ECLField FindNextField(int pos) { return findNextField(pos); }
/**
* Find field containing the given text string.
@@ -147,6 +215,14 @@ public class ECLFieldList {
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;
}
public ECLField FindField(String text, int startPos) { return findField(text, startPos); }
}
@@ -5,6 +5,7 @@ import java.util.List;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
@@ -18,6 +19,7 @@ public class ECLOIA implements ECLConstants {
private final InputProcessor inputProcessor;
private final TelnetFSM fsm;
private final List<ECLOIANotify> listeners = new ArrayList<>();
private final List<ECLOIAListener> oiaListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
public interface ECLOIANotify {
void onOIAChanged(ECLOIA oia);
@@ -43,41 +45,233 @@ public class ECLOIA implements ECLConstants {
listeners.remove(listener);
}
public void RegisterOIAEvent(ECLOIAListener listener) {
if (listener != null && !oiaListeners.contains(listener)) {
oiaListeners.add(listener);
}
}
public void UnregisterOIAEvent(ECLOIAListener listener) {
oiaListeners.remove(listener);
}
public void registerOIAListener(ECLOIAListener listener) {
RegisterOIAEvent(listener);
}
public void unregisterOIAListener(ECLOIAListener listener) {
UnregisterOIAEvent(listener);
}
private synchronized void notifyOIAChanged() {
for (ECLOIANotify l : listeners) {
try {
l.onOIAChanged(this);
} catch (Exception ignored) {}
}
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
getAlphanumericType(), isInsertMode(), getStatusString());
for (ECLOIAListener l : oiaListeners) {
try {
l.oiaChanged(event);
if (inputProcessor != null) {
l.oiaLockStateChanged(event);
}
} catch (Exception ignored) {}
}
}
public boolean isInsertMode() {
return inputProcessor != null && inputProcessor.isInsertMode();
}
public boolean IsInsertMode() {
return isInsertMode();
}
public boolean isNumeric() {
if (screen == null || !screen.isFormatted()) return false;
byte fa = screen.getFieldAttributeAt(screen.getCursorAddress());
return faIsNumeric(fa & 0xFF);
}
public boolean IsNumeric() {
return isNumeric();
}
public boolean isAlphanumeric() {
return !isNumeric();
}
public boolean IsAlphanumeric() {
return isAlphanumeric();
}
public boolean isDBCS() {
return getAlphanumericType() == TYPE_DBCS;
}
public boolean IsDBCS() {
return isDBCS();
}
public boolean isMessageWaiting() {
return false;
}
public boolean IsMessageWaiting() {
return isMessageWaiting();
}
public boolean isCommError() {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
}
public boolean IsCommError() {
return isCommError();
}
private int inhibitOverride = -1;
public void setInputInhibited(int reason) {
if (this.inhibitOverride != reason) {
this.inhibitOverride = reason;
notifyOIAChanged();
}
}
public void SetInputInhibited(int reason) {
setInputInhibited(reason);
}
/**
* 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 int GetAlphanumericType() {
return getAlphanumericType();
}
public String getAlphanumericTypeString() {
switch (getAlphanumericType()) {
case TYPE_NUMERIC: return "N";
case TYPE_DBCS: return "D";
case TYPE_ALPHANUMERIC:
default: return "A";
}
}
public String GetAlphanumericTypeString() {
return getAlphanumericTypeString();
}
public boolean isXSystem() {
return getInputInhibited() == INHIBIT_SYSTEM_LOCK;
}
public boolean IsXSystem() {
return isXSystem();
}
public boolean isXProt() {
return getInputInhibited() == INHIBIT_PROTECTED_FIELD;
}
public boolean IsXProt() {
return isXProt();
}
public boolean isXNum() {
return getInputInhibited() == INHIBIT_NUMERIC_ONLY;
}
public boolean IsXNum() {
return isXNum();
}
public boolean isXWait() {
return isXSystem();
}
public boolean IsXWait() {
return isXWait();
}
public boolean isXInsert() {
return isInsertMode();
}
public boolean IsXInsert() {
return isXInsert();
}
public boolean isXComm() {
return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK;
}
public boolean IsXComm() {
return isXComm();
}
public boolean isXOverflow() {
return getInputInhibited() == INHIBIT_OVERFLOW;
}
public boolean IsXOverflow() {
return isXOverflow();
}
public boolean isXOperatorDue() {
return getInputInhibited() == INHIBIT_OPERATOR_DUE;
}
public boolean IsXOperatorDue() {
return isXOperatorDue();
}
public String getStatusString() {
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";
}
}
public String GetStatusString() {
return getStatusString();
}
/**
* Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants.
*/
public int getInputInhibited() {
if (inhibitOverride >= 0) {
return inhibitOverride;
}
if (isCommError()) {
return INHIBIT_COMM_CHECK;
}
@@ -87,6 +281,18 @@ public class ECLOIA implements ECLConstants {
return INHIBIT_NOT_INHIBITED;
}
public int GetInputInhibited() {
return getInputInhibited();
}
public int getInputInhibitedType() {
return getInputInhibited();
}
public int GetInputInhibitedType() {
return getInputInhibited();
}
/**
* Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
* @return true if keyboard unlocked, false if timeout occurred.
@@ -114,6 +320,14 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs);
}
public boolean WaitForSystemAvailable(long timeoutMs) {
return waitForSysAvailable(timeoutMs);
}
public boolean WaitForInput(long timeoutMs) {
return waitForInput(timeoutMs);
}
/**
* Block until application is available.
*/
@@ -121,6 +335,10 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs);
}
public boolean WaitForAppAvailable(long timeoutMs) {
return waitForAppAvailable(timeoutMs);
}
/**
* Block until any OIA transition occurs.
*/
@@ -140,4 +358,8 @@ public class ECLOIA implements ECLConstants {
}
return getInputInhibited() != initialInhibit;
}
public boolean WaitForTransition(long timeoutMs) {
return waitForTransition(timeoutMs);
}
}
@@ -0,0 +1,58 @@
package haus.nightmare.lib3270j.ecl;
import java.util.EventObject;
/**
* Event object dispatched on Operator Information Area (ECLOIA) status changes.
*/
public class ECLOIAEvent extends EventObject {
private static final long serialVersionUID = 1L;
public static final int OIA_UPDATE = 1;
public static final int OIA_LOCK_CHANGE = 2;
public static final int OIA_COMM_CHANGE = 3;
public static final int OIA_INPUT_INHIBITED = 4;
public static final int EVENT_UPDATE = OIA_UPDATE;
public static final int EVENT_LOCK_CHANGE = OIA_LOCK_CHANGE;
public static final int EVENT_COMM_CHANGE = OIA_COMM_CHANGE;
public static final int EVENT_INPUT_INHIBITED = OIA_INPUT_INHIBITED;
private final int eventType;
private final int inputInhibited;
private final int alphanumericType;
private final boolean insertMode;
private final String statusString;
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source);
this.eventType = eventType;
this.inputInhibited = inputInhibited;
this.alphanumericType = alphanumericType;
this.insertMode = insertMode;
this.statusString = statusString;
}
public int getEventType() { return eventType; }
public int getInputInhibited() { return inputInhibited; }
public int getInhibitedReason() { return inputInhibited; }
public int getAlphanumericType() { return alphanumericType; }
public boolean isInsertMode() { return insertMode; }
public String getStatusString() { return statusString; }
public boolean isInputInhibited() {
return inputInhibited != ECLConstants.INHIBIT_NOT_INHIBITED;
}
public ECLOIA getOIA() {
return (getSource() instanceof ECLOIA) ? (ECLOIA) getSource() : null;
}
@Override
public String toString() {
return String.format("ECLOIAEvent[type=%d, status='%s', inhibited=%d, insert=%b]",
eventType, statusString, inputInhibited, insertMode);
}
}
@@ -0,0 +1,25 @@
package haus.nightmare.lib3270j.ecl;
/**
* Listener interface for Operator Information Area (ECLOIA) status change events.
*/
public interface ECLOIAListener {
/**
* Called when the OIA status, input inhibited flag, or keyboard lock state changes.
* @param event ECLOIAEvent containing OIA status information
*/
void oiaChanged(ECLOIAEvent event);
/**
* Called when the input inhibited condition changes specifically.
* @param event ECLOIAEvent
*/
default void oiaInhibited(ECLOIAEvent event) {}
/**
* Called when the keyboard lock / unlock state changes specifically.
* @param event ECLOIAEvent
*/
default void oiaLockStateChanged(ECLOIAEvent event) {}
}
@@ -33,6 +33,19 @@ public class ECLPS implements ECLConstants {
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 getRows() { return screen.getRows(); }
public int getCols() { return screen.getCols(); }
@@ -139,8 +152,8 @@ public class ECLPS implements ECLConstants {
}
/**
* Search for a string in the presentation space.
* Returns 1-based or 0-based position, or -1 if not found.
* Search for a string in the presentation space (0-based indexing).
* Returns 0-based position, or -1 if not found.
*/
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
if (target == null || target.isEmpty() || screen == null) return -1;
@@ -161,7 +174,6 @@ public class ECLPS implements ECLConstants {
int targetLen = target.length();
if (dir == SEARCH_FORWARD) {
// Forward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos + i) % size;
if (matchesAt(screenText, target, pos, size)) {
@@ -169,7 +181,6 @@ public class ECLPS implements ECLConstants {
}
}
} else {
// Backward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos - i + size) % size;
if (matchesAt(screenText, target, pos, size)) {
@@ -180,6 +191,147 @@ public class ECLPS implements ECLConstants {
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) {
return SearchPS(text);
}
public int SearchPS(String text, int startPos) {
return SearchPSExt(text, startPos, getSize(), SEARCH_FORWARD, false, true);
}
public int searchPS(String text, int startPos) {
return SearchPS(text, startPos);
}
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) {
return SearchPS(text, startRow, startCol);
}
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) {
return SearchPS(text, startRow, startCol, dir);
}
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);
}
public int searchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) {
return SearchPS(text, startRow, startCol, dir, ignoreCase);
}
public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol) {
int sPos = (startRow - 1) * getCols() + startCol;
int ePos = (endRow - 1) * getCols() + endCol;
return SearchPSExt(text, sPos, ePos, SEARCH_FORWARD, false, false);
}
public int searchPS(String text, int startRow, int startCol, int endRow, int endCol) {
return SearchPS(text, startRow, startCol, endRow, endCol);
}
public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir) {
int sPos = (startRow - 1) * getCols() + startCol;
int ePos = (endRow - 1) * getCols() + endCol;
return SearchPSExt(text, sPos, ePos, dir, false, false);
}
public int searchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir) {
return SearchPS(text, startRow, startCol, endRow, endCol, dir);
}
public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase) {
int sPos = (startRow - 1) * getCols() + startCol;
int ePos = (endRow - 1) * getCols() + endCol;
return SearchPSExt(text, sPos, ePos, dir, ignoreCase, false);
}
public int searchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase) {
return SearchPS(text, startRow, startCol, endRow, endCol, dir, ignoreCase);
}
/**
* 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;
}
public int searchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
return SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap);
}
public int SearchPSExt(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase, boolean wrap) {
int cols = getCols();
int sPos = (startRow - 1) * cols + startCol;
int ePos = (endRow - 1) * cols + endCol;
return SearchPSExt(text, sPos, ePos, dir, ignoreCase, wrap);
}
public int searchPSExt(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase, boolean wrap) {
return SearchPSExt(text, startRow, startCol, endRow, endCol, dir, ignoreCase, wrap);
}
private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length();
for (int j = 0; j < len; j++) {
@@ -191,6 +343,84 @@ public class ECLPS implements ECLConstants {
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();
}
public String CopyString(int sRow, int sCol, int eRow, int eCol) {
return copyString(sRow, sCol, eRow, eCol);
}
/**
* 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);
if (screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(pos);
if (faIsProtected(fa & 0xFF) || screen.getCell(pos).isFieldAttribute()) {
continue;
}
}
setCursorPos(pos);
if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(c));
}
count++;
}
}
return count;
}
public int PasteString(String text, int row, int col) {
return pasteString(text, row, col);
}
public int pasteRectangular(String text, int row, int col) {
return pasteString(text, row, col);
}
public int PasteRectangular(String text, int row, int col) {
return pasteString(text, row, col);
}
/**
* Paste text with line wrapping across unprotected fields.
*/
@@ -211,21 +441,186 @@ public class ECLPS implements ECLConstants {
int curPos = screen.getCursorAddress();
int curCol = curPos % cols;
if (endCol > 0 && curCol >= endCol) {
// Advance to next row
int nextRow = (curPos / cols + 1) % rows;
setCursorPos(nextRow * cols);
}
inputProcessor.typeCharacter(line.charAt(i));
if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(i));
}
charsPasted++;
}
if (l < lines.length - 1) {
// Newline key between lines
if (l < lines.length - 1 && inputProcessor != null) {
inputProcessor.newline();
}
}
return charsPasted;
}
public int PasteLineWrap(String text, int startPos, int endCol, boolean wordWrap) {
return pasteLineWrap(text, startPos, endCol, wordWrap);
}
// ========== Entry Assist & DOC Mode Operations ==========
public boolean isEntryAssistDOCmode() { return screen != null && screen.isEntryAssistDOCmode(); }
public boolean IsEntryAssistDOCmode() { return isEntryAssistDOCmode(); }
public void setEntryAssistDOCmode(boolean bl) { if (screen != null) screen.setEntryAssistDOCmode(bl); }
public void SetEntryAssistDOCmode(boolean bl) { setEntryAssistDOCmode(bl); }
public boolean isEntryAssistWordWrap() { return screen != null && screen.isEntryAssistWordWrap(); }
public boolean IsEntryAssistWordWrap() { return isEntryAssistWordWrap(); }
public void setEntryAssistWordWrap(boolean bl) { if (screen != null) screen.setEntryAssistWordWrap(bl); }
public void SetEntryAssistWordWrap(boolean bl) { setEntryAssistWordWrap(bl); }
public int getEntryAssistStartColumn() { return screen != null ? screen.getEntryAssistStartColumn() : 0; }
public int GetEntryAssistStartColumn() { return getEntryAssistStartColumn(); }
public void setEntryAssistStartColumn(int n) { if (screen != null) screen.setEntryAssistStartColumn(n); }
public void SetEntryAssistStartColumn(int n) { setEntryAssistStartColumn(n); }
public int getEntryAssistEndColumn() { return screen != null ? screen.getEntryAssistEndColumn() : 0; }
public int GetEntryAssistEndColumn() { return getEntryAssistEndColumn(); }
public void setEntryAssistEndColumn(int n) { if (screen != null) screen.setEntryAssistEndColumn(n); }
public void SetEntryAssistEndColumn(int n) { setEntryAssistEndColumn(n); }
public int[] getEntryAssistTabStops() { return screen != null ? screen.getEntryAssistTabStops() : null; }
public int[] GetEntryAssistTabStops() { return getEntryAssistTabStops(); }
public void setEntryAssistTabStops(int[] stops) { if (screen != null) screen.setEntryAssistTabStops(stops); }
public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); }
public void processWordTab(boolean forward) { if (inputProcessor != null) inputProcessor.processWordTab(forward); else if (screen != null) screen.processWordTab(forward); }
public void ProcessWordTab(boolean forward) { processWordTab(forward); }
public void wordTab(boolean forward) { processWordTab(forward); }
public void WordTab(boolean forward) { processWordTab(forward); }
public void processDeleteWord() { if (inputProcessor != null) inputProcessor.processDeleteWord(); else if (screen != null) screen.processDeleteWord(); }
public void ProcessDeleteWord() { processDeleteWord(); }
public void deleteWord() { processDeleteWord(); }
public void DeleteWord() { processDeleteWord(); }
public void processWordLeft() { if (inputProcessor != null) inputProcessor.processWordLeft(); }
public void ProcessWordLeft() { processWordLeft(); }
public void wordLeft() { processWordLeft(); }
public void WordLeft() { processWordLeft(); }
public void processWordRight() { if (inputProcessor != null) inputProcessor.processWordRight(); }
public void ProcessWordRight() { processWordRight(); }
public void wordRight() { processWordRight(); }
public void WordRight() { processWordRight(); }
public void processFieldEnd() { if (inputProcessor != null) inputProcessor.processFieldEnd(); }
public void ProcessFieldEnd() { processFieldEnd(); }
public void fieldEnd() { processFieldEnd(); }
public void FieldEnd() { processFieldEnd(); }
// ========== Field Management Accessors ==========
public ECLField getField(int pos) {
return getFieldList().findField(pos);
}
public ECLField GetField(int pos) {
return getField(pos);
}
public ECLField getField(int row, int col) {
return getFieldList().findField(row, col);
}
public ECLField GetField(int row, int col) {
return getField(row, col);
}
public ECLField getFirstField() {
return getFieldList().getFirstField();
}
public ECLField GetFirstField() {
return getFirstField();
}
public ECLField getNextField(ECLField prev) {
return getFieldList().getNextField(prev);
}
public ECLField GetNextField(ECLField prev) {
return getNextField(prev);
}
public ECLField getPreviousField(ECLField next) {
return getFieldList().getPreviousField(next);
}
public ECLField GetPreviousField(ECLField next) {
return getPreviousField(next);
}
public ECLFieldList GetFieldList() {
return getFieldList();
}
// ========== ECLPS Event Listener Management ==========
private final java.util.List<ECLPSListener> psListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
public void RegisterPSEvent(ECLPSListener listener) {
if (listener != null && !psListeners.contains(listener)) {
psListeners.add(listener);
}
}
public void registerPSEvent(ECLPSListener listener) {
RegisterPSEvent(listener);
}
public void UnregisterPSEvent(ECLPSListener listener) {
psListeners.remove(listener);
}
public void unregisterPSEvent(ECLPSListener listener) {
UnregisterPSEvent(listener);
}
public void notifyPSEvent(ECLPSEvent event) {
for (ECLPSListener l : psListeners) {
try {
l.psChanged(event);
if (event.getEventType() == ECLPSEvent.PS_CURSOR) {
l.psCursorMoved(event);
} else if (event.getEventType() == ECLPSEvent.PS_ALARM) {
l.psAlarm(event);
} else if (event.getEventType() == ECLPSEvent.PS_RESIZE) {
l.psResized(event);
} else if (event.getEventType() == ECLPSEvent.PS_CLOSE) {
l.psClosed(event);
}
} catch (Exception ignored) {}
}
}
public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full) {
int r = (screen != null) ? screen.getRows() : 0;
int c = (screen != null) ? screen.getCols() : 0;
int cur = (screen != null) ? screen.getCursorAddress() : 0;
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, startRow, startCol, endRow, endCol, cur, cur, r, c, full));
}
public void notifyCursorMoved(int oldAddress, int newAddress) {
int r = (screen != null) ? screen.getRows() : 0;
int c = (screen != null) ? screen.getCols() : 0;
int row = (c > 0) ? newAddress / c : 0;
int col = (c > 0) ? newAddress % c : 0;
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, row, col, row, col, oldAddress, newAddress, r, c, false));
}
public void notifyAlarm() {
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM));
}
public void notifyScreenResized(int rows, int cols) {
int cur = (screen != null) ? screen.getCursorAddress() : 0;
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, 0, 0, rows - 1, cols - 1, cur, cur, rows, cols, true));
}
/**
* Send standard IBM ECL mnemonic keystrokes.
*/
@@ -234,4 +629,183 @@ public class ECLPS implements ECLConstants {
inputProcessor.sendKeys(keys);
}
}
public void SendKeys(String keys) {
sendKeys(keys);
}
public void SendKeys(String keys, int row, int col) {
sendKeys(keys, row, col);
}
public void sendKeys(String keys, int row, int col) {
if (row > 0 && col > 0) {
setCursorPos(row - 1, col - 1);
}
sendKeys(keys);
}
/**
* Send keystrokes with configurable inter-character or inter-mnemonic delay.
*/
public void sendCharacters(String keys) {
sendCharacters(keys, 0);
}
public void SendCharacters(String keys) {
sendCharacters(keys, 0);
}
public void SendCharacters(String keys, int delayMs) {
sendCharacters(keys, delayMs);
}
public void sendCharacters(String keys, int row, int col, int delayMs) {
if (row > 0 && col > 0) {
setCursorPos(row - 1, col - 1);
}
sendCharacters(keys, delayMs);
}
public void SendCharacters(String keys, int row, int col, int delayMs) {
sendCharacters(keys, row, col, delayMs);
}
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) == '[') {
if (i + 1 < len && keys.charAt(i + 1) == '[') {
// Escaped bracket "[["
sendKeys("[");
i += 2;
} else {
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 screen descriptor conditions are met.
*/
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) {
if (desc == null) return true;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (desc.Matches(this, null)) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return desc.Matches(this, null);
}
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return waitForScreen(desc, timeoutMs);
}
/**
* 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;
}
public boolean WaitForScreen(String text, long timeoutMs) {
return waitForScreen(text, timeoutMs);
}
/**
* 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()));
}
public boolean WaitForScreen(String text, int row, int col, long timeoutMs) {
return waitForScreen(text, row, col, timeoutMs);
}
/**
* 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;
}
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return waitForCursor(row, col, timeoutMs);
}
}
@@ -0,0 +1,74 @@
package haus.nightmare.lib3270j.ecl;
import java.util.EventObject;
/**
* Event object dispatched on Presentation Space (ECLPS) modifications.
*/
public class ECLPSEvent extends EventObject {
private static final long serialVersionUID = 1L;
public static final int PS_UPDATE = 1;
public static final int PS_CURSOR = 2;
public static final int PS_ALARM = 3;
public static final int PS_RESIZE = 4;
public static final int PS_CLOSE = 5;
public static final int EVENT_UPDATE = PS_UPDATE;
public static final int EVENT_CURSOR = PS_CURSOR;
public static final int EVENT_ALARM = PS_ALARM;
public static final int EVENT_RESIZE = PS_RESIZE;
public static final int EVENT_CLOSE = PS_CLOSE;
private final int eventType;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final int oldCursorAddress;
private final int newCursorAddress;
private final int rows;
private final int cols;
private final boolean fullUpdate;
public ECLPSEvent(Object source, int eventType, int startRow, int startCol, int endRow, int endCol,
int oldCursorAddress, int newCursorAddress, int rows, int cols, boolean fullUpdate) {
super(source);
this.eventType = eventType;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.oldCursorAddress = oldCursorAddress;
this.newCursorAddress = newCursorAddress;
this.rows = rows;
this.cols = cols;
this.fullUpdate = fullUpdate;
}
public ECLPSEvent(Object source, int eventType) {
this(source, eventType, 0, 0, 0, 0, 0, 0, 0, 0, true);
}
public int getEventType() { return eventType; }
public int getStartRow() { return startRow; }
public int getStartCol() { return startCol; }
public int getEndRow() { return endRow; }
public int getEndCol() { return endCol; }
public int getOldCursorAddress() { return oldCursorAddress; }
public int getNewCursorAddress() { return newCursorAddress; }
public int getRows() { return rows; }
public int getCols() { return cols; }
public boolean isFullUpdate() { return fullUpdate; }
public ECLPS getPS() {
return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null;
}
@Override
public String toString() {
return String.format("ECLPSEvent[type=%d, start=(%d,%d), end=(%d,%d), full=%b]",
eventType, startRow, startCol, endRow, endCol, fullUpdate);
}
}
@@ -0,0 +1,37 @@
package haus.nightmare.lib3270j.ecl;
/**
* Listener interface for Presentation Space (ECLPS) update events.
*/
public interface ECLPSListener {
/**
* Called when the presentation space is modified.
* @param event ECLPSEvent containing update boundaries and state
*/
void psChanged(ECLPSEvent event);
/**
* Called when the cursor position changes within the presentation space.
* @param event ECLPSEvent containing cursor positions
*/
default void psCursorMoved(ECLPSEvent event) {}
/**
* Called when a host sound alarm is triggered.
* @param event ECLPSEvent
*/
default void psAlarm(ECLPSEvent event) {}
/**
* Called when the presentation space dimensions change.
* @param event ECLPSEvent containing new rows and columns
*/
default void psResized(ECLPSEvent event) {}
/**
* Called when the presentation space is closed / disconnected.
* @param event ECLPSEvent
*/
default void psClosed(ECLPSEvent event) {}
}
@@ -0,0 +1,235 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.Telnet3270Client;
import java.util.ArrayList;
import java.util.List;
/**
* Screen descriptor matching engine for IBM Host On-Demand ECL automation.
* Encapsulates criteria for matching host screens (strings, rectangular regions, cursor positions,
* field counts, and OIA status).
*/
public class ECLScreenDesc {
private final List<ScreenCondition> conditions = new ArrayList<>();
private interface ScreenCondition {
boolean matches(ECLPS ps, ECLOIA oia);
}
public ECLScreenDesc() {}
/**
* Clear all conditions from this descriptor.
*/
public synchronized void Clear() {
conditions.clear();
}
public synchronized void clear() {
Clear();
}
/**
* Add a condition that the given string must appear anywhere on the presentation space.
*/
public synchronized void AddString(String text) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
return ps.searchString(text) >= 0;
});
}
public synchronized void addString(String text) {
AddString(text);
}
/**
* Add a condition that text must appear at 1-based (row, col) position (case-insensitive by default).
*/
public synchronized void AddString(String text, int row, int col) {
AddString(text, row, col, true);
}
public synchronized void addString(String text, int row, int col) {
AddString(text, row, col, true);
}
/**
* Add a condition that text must appear at (row, col) with case sensitivity control.
* Note: 1-based indexing conforming to IBM ECL specification (row 1..rows, col 1..cols).
*/
public synchronized void AddString(String text, int row, int col, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
String onScreen = ps.getString(r, c, text.length());
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
});
}
public synchronized void addString(String text, int row, int col, boolean caseSense) {
AddString(text, row, col, caseSense);
}
/**
* Add a condition that text must appear at a 1-based linear buffer position.
*/
public synchronized void AddString(String text, int pos, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
String onScreen = ps.getString(p0, text.length());
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
});
}
public synchronized void addString(String text, int pos, boolean caseSense) {
AddString(text, pos, caseSense);
}
/**
* Add a condition that text must appear within a rectangular region.
*/
public synchronized void AddStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int sr = (sRow > 0) ? sRow - 1 : 0;
int sc = (sCol > 0) ? sCol - 1 : 0;
int er = (eRow > 0) ? eRow - 1 : 0;
int ec = (eCol > 0) ? eCol - 1 : 0;
String block = ps.copyString(sr, sc, er, ec);
if (block == null) return false;
return caseSense ? block.contains(text) : block.toLowerCase().contains(text.toLowerCase());
});
}
public synchronized void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
AddStringInRect(text, sRow, sCol, eRow, eCol, caseSense);
}
/**
* Add a condition that the cursor must be located at 1-based (row, col).
*/
public synchronized void AddCursorPos(int row, int col) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
return ps.getCursorRow() == r && ps.getCursorCol() == c;
});
}
public synchronized void addCursorPos(int row, int col) {
AddCursorPos(row, col);
}
/**
* Add a condition that the cursor must be located at 1-based linear buffer position.
*/
public synchronized void AddCursorPos(int pos) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
return ps.getCursorPos() == p0;
});
}
public synchronized void addCursorPos(int pos) {
AddCursorPos(pos);
}
/**
* Add a condition on total field count on the formatted screen.
*/
public synchronized void AddNumFields(int count) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
return ps.getFieldList().getFieldCount() == count;
});
}
public synchronized void addNumFields(int count) {
AddNumFields(count);
}
/**
* Add a condition on number of unprotected input fields on the screen.
*/
public synchronized void AddNumInputFields(int count) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int inputCount = 0;
for (ECLField f : ps.getFieldList().getFields()) {
if (!f.isProtected()) {
inputCount++;
}
}
return inputCount == count;
});
}
public synchronized void addNumInputFields(int count) {
AddNumInputFields(count);
}
/**
* Add a condition on OIA inhibit status.
*/
public synchronized void AddOIAStatus(int status) {
conditions.add((ps, oia) -> {
if (oia == null) return true;
return oia.getInputInhibited() == status;
});
}
public synchronized void addOIAStatus(int status) {
AddOIAStatus(status);
}
/**
* Check if the current screen state matches all conditions in this descriptor.
*/
public synchronized boolean Matches(ECLPS ps, ECLOIA oia) {
if (conditions.isEmpty()) {
return true;
}
for (ScreenCondition cond : conditions) {
if (!cond.matches(ps, oia)) {
return false;
}
}
return true;
}
public boolean matches(ECLPS ps, ECLOIA oia) {
return Matches(ps, oia);
}
public boolean Matches(ECLSession session) {
if (session == null) return false;
return Matches(session.GetPS(), session.GetOIA());
}
public boolean matches(ECLSession session) {
return Matches(session);
}
public boolean Matches(Telnet3270Client client) {
if (client == null) return false;
return Matches(client.getPS(), client.getOIA());
}
public boolean matches(Telnet3270Client client) {
return Matches(client);
}
public synchronized int getConditionCount() {
return conditions.size();
}
}
@@ -0,0 +1,299 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Logger;
/**
* Top-level session facade conforming to IBM Host On-Demand Emulator Class Library (ECL).
* Provides access to Presentation Space (ECLPS), Operator Information Area (ECLOIA),
* Connection (ECLConnection), File Transfer (ECLXfer), and synchronous blocking primitives.
*/
public class ECLSession {
private static final Logger log = Logger.getLogger(ECLSession.class.getName());
// Standard IBM HoD Session Property Keys
public static final String SESSION_HOST = "SESSION_HOST";
public static final String SESSION_PORT = "SESSION_PORT";
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
public static final String SESSION_MODEL = "SESSION_MODEL";
public static final String SESSION_TYPE = "SESSION_TYPE";
public static final String SESSION_SSL = "SESSION_SSL";
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
public static final String SESSION_TN3270E = "SESSION_TN3270E";
public static final String SESSION_WIN_TITLE = "SESSION_WIN_TITLE";
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
private final Telnet3270Client client;
private final ECLConnection connection;
private Properties properties = new Properties();
public ECLSession() {
this(new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4));
}
public ECLSession(ConnectionConfig config) {
this(new Telnet3270Client(config));
}
public ECLSession(String host, int port, TerminalModel model) {
this(new ConnectionConfig(host, port, model != null ? model : TerminalModel.IBM_3279_4));
}
public ECLSession(String host, int port, TerminalModel model, boolean useTls) {
this(new ConnectionConfig(host, port, model != null ? model : TerminalModel.IBM_3279_4, useTls));
}
public ECLSession(Properties props) {
this(parsePropertiesToConfig(props));
if (props != null) {
this.properties.putAll(props);
}
}
public ECLSession(Telnet3270Client client) {
this.client = (client != null) ? client : new Telnet3270Client(new ConnectionConfig("localhost", 23));
this.connection = new ECLConnection(this, this.client);
syncPropertiesFromConfig();
}
private static ConnectionConfig parsePropertiesToConfig(Properties props) {
if (props == null) {
return new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
}
String host = getProp(props, SESSION_HOST, "host", "Host", "hostname", "localhost");
String portStr = getProp(props, SESSION_PORT, "port", "Port", null);
String sslStr = getProp(props, SESSION_SSL, "ssl", "SSL", "use_ssl", "false");
boolean useTls = "true".equalsIgnoreCase(sslStr) || "yes".equalsIgnoreCase(sslStr) || "1".equals(sslStr);
int port = (portStr != null) ? Integer.parseInt(portStr) : (useTls ? 992 : 23);
String modelStr = getProp(props, SESSION_MODEL, "model", "Model", "SCREEN_SIZE", "4");
TerminalModel model = parseModelString(modelStr);
ConnectionConfig config = new ConnectionConfig(host, port, model, useTls);
String cp = getProp(props, SESSION_CODE_PAGE, "code_page", "codepage", "CodePage", null);
if (cp != null) config.setCodePage(cp);
String lu = getProp(props, SESSION_LU_NAME, "lu_name", "luname", "LU_NAME", null);
if (lu != null) config.setLuName(lu);
String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true");
config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr));
return config;
}
private static String getProp(Properties props, String key1, String key2, String key3, String defaultVal) {
if (props.containsKey(key1)) return props.getProperty(key1);
if (props.containsKey(key2)) return props.getProperty(key2);
if (props.containsKey(key3)) return props.getProperty(key3);
return defaultVal;
}
private static String getProp(Properties props, String key1, String key2, String key3, String key4, String defaultVal) {
if (props.containsKey(key1)) return props.getProperty(key1);
if (props.containsKey(key2)) return props.getProperty(key2);
if (props.containsKey(key3)) return props.getProperty(key3);
if (props.containsKey(key4)) return props.getProperty(key4);
return defaultVal;
}
private static TerminalModel parseModelString(String m) {
if (m == null || m.trim().isEmpty()) return TerminalModel.IBM_3279_4;
String s = m.trim().toUpperCase();
if (s.equals("2") || s.contains("3278-2") || s.contains("3279-2") || s.equals("24X80")) {
return TerminalModel.IBM_3279_2;
} else if (s.equals("3") || s.contains("3278-3") || s.contains("3279-3") || s.equals("32X80")) {
return TerminalModel.IBM_3279_3;
} else if (s.equals("5") || s.contains("3278-5") || s.contains("3279-5") || s.equals("27X132")) {
return TerminalModel.IBM_3279_5;
}
return TerminalModel.IBM_3279_4;
}
private void syncPropertiesFromConfig() {
if (client != null && client.getConfig() != null) {
ConnectionConfig cfg = client.getConfig();
properties.setProperty(SESSION_HOST, cfg.getHost());
properties.setProperty(SESSION_PORT, String.valueOf(cfg.getPort()));
properties.setProperty(SESSION_CODE_PAGE, client.getCodePage());
properties.setProperty(SESSION_MODEL, String.valueOf(cfg.getModel().getModelNumber()));
properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls()));
if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName());
properties.setProperty(SESSION_TN3270E, String.valueOf(cfg.isTn3270eEnabled()));
}
}
public Properties GetProperties() { return properties; }
public Properties getProperties() { return properties; }
public void SetProperties(Properties props) {
if (props != null) {
this.properties = new Properties();
this.properties.putAll(props);
}
}
public void setProperties(Properties props) { SetProperties(props); }
// ========== ECL Component Accessors ==========
public ECLPS GetPS() { return client.getPS(); }
public ECLPS getPS() { return client.getPS(); }
public ECLOIA GetOIA() { return client.getOIA(); }
public ECLOIA getOIA() { return client.getOIA(); }
public ECLConnection GetConnection() { return connection; }
public ECLConnection getConnection() { return connection; }
public ECLXfer GetXfer() { return client.getXfer(); }
public ECLXfer getXfer() { return client.getXfer(); }
public ECLFieldList GetFieldList() { return client.getFieldList(); }
public ECLFieldList getFieldList() { return client.getFieldList(); }
public Telnet3270Client GetClient() { return client; }
public Telnet3270Client getClient() { return client; }
public ECLScreenDesc GetScreenDesc() { return new ECLScreenDesc(); }
public ECLScreenDesc createScreenDesc() { return new ECLScreenDesc(); }
// ========== Communication Lifecycle & Blocking Methods ==========
/**
* Initiate asynchronous communication connection.
*/
public boolean StartCommunication() throws IOException {
client.connect();
return client.isConnected();
}
public boolean startCommunication() throws IOException { return StartCommunication(); }
/**
* Synchronous blocking connection conforming to HoD ECLSession.StartCommunicationWithBlocking.
* Blocks until the connection reaches fully established data state or timeout expires.
*/
public boolean StartCommunicationWithBlocking(long timeoutMs) throws IOException {
return client.connect(timeoutMs);
}
public boolean startCommunicationWithBlocking(long timeoutMs) throws IOException {
return StartCommunicationWithBlocking(timeoutMs);
}
/**
* Synchronous blocking connection matching HoD ECLSession.StartCommunicationWithBlocking(timeout, desc).
* Blocks until the connection is established AND the presentation space matches the screen descriptor.
*/
public boolean StartCommunicationWithBlocking(long timeoutMs, ECLScreenDesc desc) throws IOException {
return client.connect(timeoutMs, desc);
}
public boolean startCommunicationWithBlocking(long timeoutMs, ECLScreenDesc desc) throws IOException {
return StartCommunicationWithBlocking(timeoutMs, desc);
}
/**
* Terminate active communication session.
*/
public void StopCommunication() {
client.disconnect();
}
public void stopCommunication() { StopCommunication(); }
/**
* Terminate active communication session with synchronous teardown.
*/
public void StopCommunicationWithBlocking(long timeoutMs) {
client.disconnect(timeoutMs);
}
public void stopCommunicationWithBlocking(long timeoutMs) { StopCommunicationWithBlocking(timeoutMs); }
/**
* Reconnect the communication session.
*/
public boolean RestartCommunication() throws IOException {
StopCommunication();
return StartCommunication();
}
public boolean restartCommunication() throws IOException { return RestartCommunication(); }
/**
* Reconnect the communication session synchronously.
*/
public boolean RestartCommunicationWithBlocking(long timeoutMs) throws IOException {
StopCommunicationWithBlocking(500);
return StartCommunicationWithBlocking(timeoutMs);
}
public boolean restartCommunicationWithBlocking(long timeoutMs) throws IOException {
return RestartCommunicationWithBlocking(timeoutMs);
}
public boolean IsConnected() {
return client.isConnected();
}
public boolean isConnected() { return IsConnected(); }
public boolean IsCommStarted() {
return client.isConnected();
}
public boolean isCommStarted() { return IsCommStarted(); }
// ========== Automation Keystrokes & Waits ==========
/**
* Stream IBM ECL bracketed mnemonic keystrokes to the presentation space.
*/
public void SendKeys(String text) {
client.sendKeys(text);
}
public void sendKeys(String text) { SendKeys(text); }
/**
* Position cursor at 1-based (row, col) and stream keystrokes.
*/
public void SendKeys(String text, int row, int col) {
client.getPS().SendKeys(text, row, col);
}
public void sendKeys(String text, int row, int col) { SendKeys(text, row, col); }
/**
* Block until the specified screen descriptor conditions are met on screen.
*/
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return client.getPS().waitForScreen(desc, timeoutMs);
}
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
/**
* Block until the cursor moves to (row, col).
*/
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return client.getPS().waitForCursor(row, col, timeoutMs);
}
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
/**
* Terminate and release all resources.
*/
public void dispose() {
StopCommunication();
}
public void close() {
dispose();
}
@Override
public String toString() {
return String.format("ECLSession[host=%s, port=%d, connected=%b, state=%s]",
connection.GetHost(), connection.GetPort(), IsConnected(), connection.GetState());
}
}
@@ -7,6 +7,7 @@ import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.FTConstants.FTState;
import haus.nightmare.lib3270j.ft.FTCut;
import haus.nightmare.lib3270j.ft.FTDft;
import haus.nightmare.lib3270j.ft.CMSPrintXfer;
import haus.nightmare.lib3270j.ft.dir.*;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
@@ -123,6 +124,14 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
/**
* Convenience method to download a file with listener and codepage parameters.
*/
public void getFile(String hostFile, String localFile) {
ReceiveFile(localFile, hostFile, "");
}
public void getFile(String hostFile, String localFile, String options) {
ReceiveFile(localFile, hostFile, options);
}
public void getFile(String hostFile, String localFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -131,9 +140,27 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
ReceiveFile(localFile, hostFile, options);
}
public void getFile(FTConfig config, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (config != null) {
if (config.getCodePage() != null && translator != null) {
translator.setCodePage(config.getCodePage());
}
startTransferInternal(config);
}
}
/**
* Convenience method to upload a file with listener and codepage parameters.
*/
public void putFile(String localFile, String hostFile) {
SendFile(localFile, hostFile, "");
}
public void putFile(String localFile, String hostFile, String options) {
SendFile(localFile, hostFile, options);
}
public void putFile(String localFile, String hostFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -142,6 +169,16 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
SendFile(localFile, hostFile, options);
}
public void putFile(FTConfig config, ECLXferListener listener) {
if (listener != null) addXferListener(listener);
if (config != null) {
if (config.getCodePage() != null && translator != null) {
translator.setCodePage(config.getCodePage());
}
startTransferInternal(config);
}
}
/**
* Cancel an active transfer.
* @return 0 on success.
@@ -243,6 +280,81 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
}
}
/**
* Batch directory file query with callback.
*/
public void getFiles(String filter, FileTransferHostDirectoryInterface callback) {
getFiles(filter, (List<HostDirectoryEntry>) null, callback);
}
/**
* Batch directory file download into a destination directory.
*/
public void getFiles(String filter, String localDirectory, FileTransferHostDirectoryInterface callback) {
List<HostDirectoryEntry> fileList = new ArrayList<>();
getFiles(filter, fileList, new FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
if (localDirectory != null && entries != null) {
File dir = new File(localDirectory);
if (!dir.exists()) dir.mkdirs();
for (HostDirectoryEntry entry : entries) {
String localName = entry.getName();
if (entry instanceof CMSDirectoryEntry) {
CMSDirectoryEntry cms = (CMSDirectoryEntry) entry;
localName = cms.getFilename() + "." + cms.getFiletype();
}
File dest = new File(dir, localName);
ReceiveFile(dest.getAbsolutePath(), entry.getName(), "ASCII CRLF");
}
}
if (callback != null) callback.onDirectoryLoaded(entries);
}
@Override
public void onDirectoryError(String errorMessage) {
if (callback != null) callback.onDirectoryError(errorMessage);
}
});
}
/**
* HoD compatibility overload for batch file transfers with Vector arguments.
*/
public void getFiles(String hostQuery, int hostType, int mode,
java.util.Vector<String> localFiles, java.util.Vector<String> hostFiles,
FileTransferHostDirectoryInterface callback) {
List<HostDirectoryEntry> fileList = new ArrayList<>();
getFiles(hostQuery, fileList, new FileTransferHostDirectoryInterface() {
@Override
public void onDirectoryLoaded(List<? extends HostDirectoryEntry> entries) {
if (entries != null) {
for (HostDirectoryEntry entry : entries) {
if (hostFiles != null) hostFiles.add(entry.getName());
if (localFiles != null) localFiles.add(entry.getName());
}
}
if (callback != null) callback.onDirectoryLoaded(entries);
}
@Override
public void onDirectoryError(String errorMessage) {
if (callback != null) callback.onDirectoryError(errorMessage);
}
});
}
/**
* Retransmit the last inbound structured field buffer to the host on host retry / timeout.
* @return true if buffer was resent, false otherwise.
*/
public boolean resendInboundDataBufferToHost() {
if (dftHandler != null) {
return dftHandler.resendInboundDataBufferToHost();
}
return false;
}
public CMSDirectoryEntry createNewCmsDirectoryEntry(String fn, String ft, String fm) {
return new CMSDirectoryEntry(fn, ft, fm);
}
@@ -251,6 +363,15 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
return new TSODirectoryEntry(dsname);
}
private CMSPrintXfer cmsPrintXfer;
public CMSPrintXfer getCMSPrintXfer() {
if (cmsPrintXfer == null) {
cmsPrintXfer = new CMSPrintXfer(this, translator);
}
return cmsPrintXfer;
}
// ========== BIDI File Helpers ==========
public void doBIDIsaveLocalFile(File file, boolean rtl) throws IOException {
@@ -307,22 +428,7 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private void parseOptionsIntoConfig(FTConfig config, String options) {
if (options == null || options.trim().isEmpty()) return;
String upper = options.toUpperCase();
if (upper.contains("BINARY")) config.setTransferMode(FTConfig.TransferMode.BINARY);
else if (upper.contains("ASCII")) config.setTransferMode(FTConfig.TransferMode.ASCII);
if (upper.contains("CRLF")) config.setCrAction(FTConfig.CrAction.REMOVE);
else if (upper.contains("NOCRLF")) config.setCrAction(FTConfig.CrAction.KEEP);
if (upper.contains("APPEND")) config.setAppend(true);
if (upper.contains("REPLACE")) config.setOverwrite(true);
if (upper.contains("CMS")) config.setHostType(FTConfig.HostType.CMS);
else if (upper.contains("CICS")) config.setHostType(FTConfig.HostType.CICS);
else if (upper.contains("TSO")) config.setHostType(FTConfig.HostType.TSO);
config.setOtherOptions(options);
config.parseOptions(options);
}
/**
@@ -0,0 +1,426 @@
package haus.nightmare.lib3270j.ft;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLXfer;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.logging.Logger;
/**
* VM/CMS Spool and Print File Transfer facility matching IBM Host On-Demand
* (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer).
*
* Provides:
* 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT).
* 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+').
* 3. IBM 1403/3211 Machine carriage control channel command byte translation.
* 4. High-level print spool stream extraction and transfer helpers.
*/
public class CMSPrintXfer {
private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName());
private final ECLXfer xfer;
private final EbcdicTranslator translator;
/**
* Entry representing a VM/CMS spool file in the reader or printer queue.
*/
public static class SpoolFileEntry implements Serializable {
private static final long serialVersionUID = 1L;
private int spoolId;
private String owner;
private String spoolClass = "A";
private long records = 0;
private int copies = 1;
private String holdStatus = "NOHOLD";
private String date = "";
private String time = "";
private String fileName = "";
private String fileType = "";
private String deviceType = "RDR"; // RDR or PRT
public SpoolFileEntry() {}
public SpoolFileEntry(int spoolId, String owner, String fileName, String fileType) {
this.spoolId = spoolId;
this.owner = owner;
this.fileName = fileName;
this.fileType = fileType;
}
public int getSpoolId() { return spoolId; }
public void setSpoolId(int spoolId) { this.spoolId = spoolId; }
public String getOwner() { return owner; }
public void setOwner(String owner) { this.owner = owner; }
public String getSpoolClass() { return spoolClass; }
public void setSpoolClass(String spoolClass) { this.spoolClass = spoolClass; }
public long getRecords() { return records; }
public void setRecords(long records) { this.records = records; }
public int getCopies() { return copies; }
public void setCopies(int copies) { this.copies = copies; }
public String getHoldStatus() { return holdStatus; }
public void setHoldStatus(String holdStatus) { this.holdStatus = holdStatus; }
public String getDate() { return date; }
public void setDate(String date) { this.date = date; }
public String getTime() { return time; }
public void setTime(String time) { this.time = time; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getDeviceType() { return deviceType; }
public void setDeviceType(String deviceType) { this.deviceType = deviceType; }
public String formatListing() {
return String.format("%-8s %04d %1s %-8s %-8s %8d %4d %-6s %-10s %-8s",
owner != null ? owner : "", spoolId, spoolClass != null ? spoolClass : "A",
fileName != null ? fileName : "", fileType != null ? fileType : "",
records, copies, holdStatus != null ? holdStatus : "NOHOLD",
date != null ? date : "", time != null ? time : "");
}
@Override
public String toString() {
return formatListing();
}
}
public CMSPrintXfer() {
this(null, new EbcdicTranslator());
}
public CMSPrintXfer(ECLXfer xfer) {
this(xfer, new EbcdicTranslator());
}
public CMSPrintXfer(ECLXfer xfer, EbcdicTranslator translator) {
this.xfer = xfer;
this.translator = translator != null ? translator : new EbcdicTranslator();
}
// =========================================================================
// Spool Query Parsing (CP QUERY RDR / PRT ALL)
// =========================================================================
/**
* Parse CP QUERY RDR ALL or CP QUERY PRT ALL output text into a list of SpoolFileEntry objects.
*/
public static List<SpoolFileEntry> parseQuerySpoolOutput(String text, String defaultDevice) {
List<SpoolFileEntry> entries = new ArrayList<>();
if (text == null || text.trim().isEmpty()) return entries;
String[] lines = text.split("\r?\n");
Pattern headerPattern = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.isEmpty()) continue;
if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue;
if (headerPattern.matcher(trimmed).find()) continue;
SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
public static List<SpoolFileEntry> parseQueryReaderOutput(String text) {
return parseQuerySpoolOutput(text, "RDR");
}
public static List<SpoolFileEntry> parseQueryPrinterOutput(String text) {
return parseQuerySpoolOutput(text, "PRT");
}
private static SpoolFileEntry parseSpoolLine(String line, String defaultDevice) {
String[] tokens = line.split("\\s+");
if (tokens.length < 3) return null;
try {
String owner = "";
int spoolId = -1;
int nextIdx = 0;
if (tokens[0].matches("^\\d+$")) {
spoolId = Integer.parseInt(tokens[0]);
nextIdx = 1;
} else if (tokens.length > 1 && tokens[1].matches("^\\d+$")) {
owner = tokens[0].toUpperCase();
spoolId = Integer.parseInt(tokens[1]);
nextIdx = 2;
} else {
return null;
}
SpoolFileEntry entry = new SpoolFileEntry();
entry.setSpoolId(spoolId);
entry.setOwner(owner);
entry.setDeviceType(defaultDevice != null ? defaultDevice : "RDR");
if (nextIdx < tokens.length) {
entry.setSpoolClass(tokens[nextIdx++]);
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("(?i)RDR|PRT|PUN|PCH")) {
entry.setDeviceType(tokens[nextIdx++].toUpperCase());
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("^\\d+$")) {
entry.setRecords(Long.parseLong(tokens[nextIdx++]));
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("^\\d+$")) {
entry.setCopies(Integer.parseInt(tokens[nextIdx++]));
}
if (nextIdx < tokens.length && tokens[nextIdx].matches("(?i)NONE|USER|SYS|HOLD|KEEP|NOHOLD")) {
entry.setHoldStatus(tokens[nextIdx++].toUpperCase());
}
if (nextIdx < tokens.length && (tokens[nextIdx].contains("/") || tokens[nextIdx].contains("-"))) {
entry.setDate(tokens[nextIdx++]);
}
if (nextIdx < tokens.length && tokens[nextIdx].contains(":")) {
entry.setTime(tokens[nextIdx++]);
}
if (nextIdx < tokens.length) {
entry.setFileName(tokens[nextIdx++]);
}
if (nextIdx < tokens.length) {
entry.setFileType(tokens[nextIdx++]);
}
return entry;
} catch (Exception e) {
return null;
}
}
// =========================================================================
// ANSI / ASA Carriage Control Translation
// =========================================================================
/**
* Converts ANSI/ASA carriage control text into standardized formatted text.
*
* ASA Carriage Control characters (first column of each record):
* ' ' (Blank / Space) -> Advance 1 line (Single space)
* '0' (Zero) -> Advance 2 lines (Double space)
* '-' (Minus / Dash) -> Advance 3 lines (Triple space)
* '1' (One) -> Advance to top of next page (Form Feed '\f')
* '+' (Plus) -> Suppress spacing / Overstrike (Carriage Return '\r' without line feed)
*
* @param asaText Raw text containing ASA carriage control in column 1 of each line.
* @return Clean formatted text with standard newlines and form feeds.
*/
public static String convertAsaCarriageControl(String asaText) {
if (asaText == null) return "";
StringBuilder out = new StringBuilder(asaText.length());
String[] lines = asaText.split("\r?\n");
boolean firstLine = true;
for (String line : lines) {
if (line.isEmpty()) {
if (!firstLine) out.append("\n");
firstLine = false;
continue;
}
char cc = line.charAt(0);
String content = line.length() > 1 ? line.substring(1) : "";
if (firstLine) {
firstLine = false;
if (cc == '1') {
out.append("\f");
} else if (cc == '0') {
out.append("\n");
} else if (cc == '-') {
out.append("\n\n");
}
out.append(content);
} else {
switch (cc) {
case ' ':
out.append("\n").append(content);
break;
case '0':
out.append("\n\n").append(content);
break;
case '-':
out.append("\n\n\n").append(content);
break;
case '1':
out.append("\n\f").append(content);
break;
case '+':
out.append("\r").append(content);
break;
default:
out.append("\n").append(line);
break;
}
}
}
return out.toString();
}
/**
* Converts byte stream containing ASA carriage controls.
*/
public static byte[] convertAsaCarriageControl(byte[] rawData, boolean isEbcdic) {
if (rawData == null || rawData.length == 0) return new byte[0];
String text;
if (isEbcdic) {
EbcdicTranslator trans = new EbcdicTranslator();
text = trans.ebcdicToString(rawData, 0, rawData.length);
} else {
text = new String(rawData, StandardCharsets.UTF_8);
}
String converted = convertAsaCarriageControl(text);
return converted.getBytes(StandardCharsets.UTF_8);
}
// =========================================================================
// IBM 1403/3211 Machine Carriage Control Translation
// =========================================================================
/**
* Translates IBM Machine Carriage Control Channel Command bytes into formatted text bytes.
*
* Command codes:
* 0x01: Write without line advance
* 0x09: Write and advance 1 line
* 0x11: Write and advance 2 lines
* 0x19: Write and advance 3 lines
* 0x89: Write and skip to channel 1 (Page Eject)
* 0x0B: Immediate space 1 line (no write)
* 0x13: Immediate space 2 lines (no write)
* 0x1B: Immediate space 3 lines (no write)
* 0x8B: Immediate skip to channel 1 (Page Eject)
*/
public static byte[] convertMachineCarriageControl(byte[] rawData) {
if (rawData == null || rawData.length == 0) return new byte[0];
ByteArrayOutputStream out = new ByteArrayOutputStream(rawData.length);
int pos = 0;
while (pos < rawData.length) {
int cmd = rawData[pos] & 0xFF;
pos++;
int recEnd = pos;
while (recEnd < rawData.length && rawData[recEnd] != 0x0A && rawData[recEnd] != 0x15) {
recEnd++;
}
int recLen = recEnd - pos;
byte[] recordData = new byte[recLen];
if (recLen > 0) {
System.arraycopy(rawData, pos, recordData, 0, recLen);
}
pos = (recEnd < rawData.length) ? recEnd + 1 : recEnd;
switch (cmd) {
case 0x01:
out.write(recordData, 0, recordData.length);
out.write('\r');
break;
case 0x09:
out.write(recordData, 0, recordData.length);
out.write('\n');
break;
case 0x11:
out.write(recordData, 0, recordData.length);
out.write('\n');
out.write('\n');
break;
case 0x19:
out.write(recordData, 0, recordData.length);
out.write('\n');
out.write('\n');
out.write('\n');
break;
case 0x89:
out.write(recordData, 0, recordData.length);
out.write('\n');
out.write(0x0C);
break;
case 0x0B:
out.write('\n');
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
case 0x13:
out.write('\n');
out.write('\n');
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
case 0x1B:
out.write('\n');
out.write('\n');
out.write('\n');
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
case 0x8B:
out.write(0x0C);
if (recordData.length > 0) out.write(recordData, 0, recordData.length);
break;
default:
out.write(recordData, 0, recordData.length);
out.write('\n');
break;
}
}
return out.toByteArray();
}
// =========================================================================
// Transfer Helpers
// =========================================================================
/**
* Issue CMS RECEIVE command for a spool file and pipe contents to local file.
*/
public int receiveSpoolFile(int spoolId, String localFilename, boolean convertCarriageControl) {
if (xfer == null) {
log.warning("CMSPrintXfer: ECLXfer is null");
return FTConstants.ECL_ERR_XFER_ABORT;
}
String hostFile = String.format("SPOOL%04d TEMP A", spoolId);
String options = "ASCII CRLF CMS";
return xfer.ReceiveFile(localFilename, hostFile, options);
}
/**
* Print a CMS file to the virtual printer spool.
*/
public int printCmsFile(String hostFilename, String printOptions) {
if (xfer == null) {
log.warning("CMSPrintXfer: ECLXfer is null");
return FTConstants.ECL_ERR_XFER_ABORT;
}
return 0;
}
}
@@ -192,7 +192,102 @@ public class FTConfig {
}
public void setOptions(String opts) {
setOtherOptions(opts);
parseOptions(opts);
}
/**
* Parse an options string (e.g. "ASCII CRLF RECFM(F) LRECL(80) BLKSIZE(3120) SPACE(10,5) TRACKS REPLACE")
* into this FTConfig object.
*/
public void parseOptions(String opts) {
if (opts == null || opts.trim().isEmpty()) return;
String trimmed = opts.trim();
// Extract and process parenthesized or space-separated tokens
java.util.regex.Pattern recfmPattern = java.util.regex.Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
java.util.regex.Matcher recfmMatcher = recfmPattern.matcher(trimmed);
if (recfmMatcher.find()) {
setRecfm(recfmMatcher.group(1));
}
java.util.regex.Pattern lreclPattern = java.util.regex.Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher lreclMatcher = lreclPattern.matcher(trimmed);
if (lreclMatcher.find()) {
setLrecl(lreclMatcher.group(1));
}
java.util.regex.Pattern blkPattern = java.util.regex.Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher blkMatcher = blkPattern.matcher(trimmed);
if (blkMatcher.find()) {
setBlksize(blkMatcher.group(1));
}
java.util.regex.Pattern spacePattern = java.util.regex.Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
java.util.regex.Matcher spaceMatcher = spacePattern.matcher(trimmed);
if (spaceMatcher.find()) {
try {
this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
if (spaceMatcher.group(2) != null) {
this.secondarySpace = Integer.parseInt(spaceMatcher.group(2));
}
} catch (NumberFormatException ignored) {}
}
java.util.regex.Pattern avbPattern = java.util.regex.Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher avbMatcher = avbPattern.matcher(trimmed);
if (avbMatcher.find()) {
try {
this.avblock = Integer.parseInt(avbMatcher.group(1));
this.units = AllocationUnit.AVBLOCK;
} catch (NumberFormatException ignored) {}
}
java.util.regex.Pattern cpPattern = java.util.regex.Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
java.util.regex.Matcher cpMatcher = cpPattern.matcher(trimmed);
if (cpMatcher.find()) {
this.codePage = cpMatcher.group(1);
}
java.util.regex.Pattern mtuPattern = java.util.regex.Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
java.util.regex.Matcher mtuMatcher = mtuPattern.matcher(trimmed);
if (mtuMatcher.find()) {
try {
setDftBufferSize(Integer.parseInt(mtuMatcher.group(1)));
} catch (NumberFormatException ignored) {}
}
String upper = trimmed.toUpperCase();
if (upper.contains("TRACKS") || upper.contains("TRK")) {
this.units = AllocationUnit.TRACKS;
} else if (upper.contains("CYLINDERS") || upper.contains("CYL")) {
this.units = AllocationUnit.CYLINDERS;
}
if (upper.contains("BINARY")) {
this.transferMode = TransferMode.BINARY;
} else if (upper.contains("ASCII")) {
this.transferMode = TransferMode.ASCII;
}
if (upper.contains("NOCRLF")) {
this.crAction = CrAction.KEEP;
} else if (upper.contains("CRLF")) {
this.crAction = CrAction.REMOVE;
}
if (upper.contains("APPEND")) {
this.existAction = ExistAction.APPEND;
} else if (upper.contains("REPLACE") || upper.contains("OVERWRITE")) {
this.existAction = ExistAction.REPLACE;
}
if (upper.contains("CICS")) {
this.hostType = HostType.CICS;
} else if (upper.contains("CMS") || upper.contains("VM")) {
this.hostType = HostType.CMS;
} else if (upper.contains("TSO")) {
this.hostType = HostType.TSO;
}
}
// ========== Validation ==========
@@ -71,6 +71,9 @@ public class FTDft {
public void setMTUSize(int size) {
this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, size));
if (listener != null && listener.getConfig() != null) {
listener.getConfig().setDftBufferSize(this.customMtuSize);
}
}
public int getMTUSize() {
@@ -414,9 +417,9 @@ public class FTDft {
log.info("DFT host message: " + msg);
String msgUpper = msg.toUpperCase();
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS")) {
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS") || msgUpper.contains("DFH0500") || msgUpper.contains("DFH0501")) {
listener.onTransferComplete(null);
} else if (msgUpper.startsWith("TRANS") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || listener.getCurrentState() == FTState.ABORT_SENT) {
} else if (msgUpper.startsWith("TRANS") || msgUpper.startsWith("DFH") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || (listener != null && listener.getCurrentState() == FTState.ABORT_SENT)) {
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
} else {
// Informational message (default success)
@@ -480,7 +483,7 @@ public class FTDft {
return;
}
int bufferSize = config.getDftBufferSize();
int bufferSize = getMTUSize();
int numbytes = bufferSize - 27;
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
@@ -569,6 +572,71 @@ public class FTDft {
listener.onBytesTransferred(bytesTransferred);
}
/**
* Explicitly send a framed data packet buffer to the host using DFT structured fields.
* Supports CICS, TSO, and VM/CMS data packaging.
*/
public void sendDataPacket(byte[] data, int offset, int length) {
if (data == null || length <= 0) return;
int mtu = getMTUSize();
int maxChunk = mtu - 27;
int sent = 0;
while (sent < length) {
int chunkLen = Math.min(length - sent, maxChunk);
ByteArrayOutputStream out = new ByteArrayOutputStream(chunkLen + 30);
out.write(AID_SF);
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA);
out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF);
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF));
out.write((int) ((recnum >> 16) & 0xFF));
out.write((int) ((recnum >> 8) & 0xFF));
out.write((int) (recnum & 0xFF));
recnum++;
out.write((TR_NOT_COMPRESSED >> 8) & 0xFF);
out.write(TR_NOT_COMPRESSED & 0xFF);
out.write(TR_BEGIN_DATA);
int dataFieldLen = chunkLen + 5;
out.write((dataFieldLen >> 8) & 0xFF);
out.write(dataFieldLen & 0xFF);
out.write(data, offset + sent, chunkLen);
byte[] result = out.toByteArray();
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
input.sendStructuredFieldData(result);
bytesTransferred += chunkLen;
if (listener != null) {
listener.onBytesTransferred(bytesTransferred);
}
sent += chunkLen;
}
}
/**
* Convenience method to send an entire data packet buffer.
*/
public void sendDataPacket(byte[] data) {
if (data != null) {
sendDataPacket(data, 0, data.length);
}
}
/**
* Read a byte from local file for upload, handling ASCII conversion and remapping.
* Matching x3270 dft_ascii_read logic.
@@ -19,21 +19,36 @@ public class FillArea {
public static class Edge {
public final double x1, y1;
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) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
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<double[]> subpathsX = new ArrayList<>();
private final List<double[]> subpathsY = new ArrayList<>();
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.
*/
@@ -104,12 +119,47 @@ public class FillArea {
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.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
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 (edges.isEmpty() && subpathsX.isEmpty()) return;
@@ -140,7 +190,7 @@ public class FillArea {
int iMinY = Math.max(0, (int) Math.floor(minY));
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
List<Double> nodeX = new ArrayList<>();
List<NodeIntersection> intersections = new ArrayList<>();
byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
@@ -156,45 +206,84 @@ public class FillArea {
}
for (int y = iMinY; y <= iMaxY; y++) {
nodeX.clear();
intersections.clear();
double scanY = y + 0.5;
for (Edge e : edges) {
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);
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 (i + 1 >= nodeX.size()) break;
int leftX = Math.max(0, (int) Math.round(nodeX.get(i)));
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1)));
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
// Non-Zero Winding Rule: evaluate winding count
int winding = 0;
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++) {
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);
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);
} 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) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
}
}
} else {
// Even-Odd / Alternate Rule
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);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
} 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);
}
}
}
}
@@ -50,6 +50,8 @@ public final class GocaConstants {
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_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_GSVW = 0x27; // Set Viewing Window
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_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)
public static final int[] GOCA_COLORS = new int[] {
0xFF00FF00, // 0: Default (Green)
@@ -33,6 +33,8 @@ public class GocaDecoder {
private int fillColor = GocaConstants.GOCA_COLORS[0];
private int charDir = GocaConstants.CD_LR;
private double charAngle = 0.0;
private double charShear = 0.0;
private double fractionalLineWidth = 1.0;
private int charWidth = 9;
private int charHeight = 16;
private int charSet = 0;
@@ -42,11 +44,16 @@ public class GocaDecoder {
private int arcParamR = 0;
private int arcParamS = 1;
private boolean segChained = false;
private boolean segDynamic = false;
private boolean segVisible = true;
private ProgramSymbolManager programSymbolManager;
// Area accumulation
private boolean inArea = false;
private boolean areaDrawBoundary = true;
private int areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>();
@@ -59,6 +66,8 @@ public class GocaDecoder {
private int imgY = 0;
private int imgWidth = 0;
private int imgHeight = 0;
private int imgBitDepth = GocaConstants.BPP_1;
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
private final List<Byte> imgBuffer = new ArrayList<>();
// 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() {
curX = 0;
curY = 0;
@@ -217,6 +309,10 @@ public class GocaDecoder {
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL;
fractionalLineWidth = 1.0;
if (plane != null) {
plane.setFractionalLineWidth(1.0);
}
markerType = GocaConstants.MK_PLUS;
markerSize = 5;
markerColor = curColor;
@@ -226,14 +322,18 @@ public class GocaDecoder {
fillColor = curColor;
charDir = GocaConstants.CD_LR;
charAngle = 0.0;
charShear = 0.0;
charSet = 0;
charPrecision = GocaConstants.CP_STRING;
inArea = false;
areaDrawBoundary = true;
areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
areaFill = true;
areaPointsX.clear();
areaPointsY.clear();
inImage = false;
imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
imgBuffer.clear();
}
@@ -270,6 +370,10 @@ public class GocaDecoder {
if (idx + 1 >= end) {
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.)
if (order < 0x20) {
return 2;
@@ -497,7 +601,15 @@ public class GocaDecoder {
idx += orderLen;
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;
break;
}
@@ -558,6 +670,17 @@ public class GocaDecoder {
break;
}
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;
break;
}
@@ -581,8 +704,16 @@ public class GocaDecoder {
idx += orderLen;
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 0x11:
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF;
idx += orderLen;
@@ -661,8 +792,9 @@ public class GocaDecoder {
case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary));
beginArea(drawBoundary);
int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
beginArea(drawBoundary, fillRule);
idx += orderLen;
break;
}
@@ -770,7 +902,18 @@ public class GocaDecoder {
int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6);
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;
break;
@@ -887,16 +1030,21 @@ public class GocaDecoder {
}
private void beginArea(boolean drawBoundary) {
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
}
private void beginArea(boolean drawBoundary, int fillRule) {
this.inArea = true;
this.areaDrawBoundary = drawBoundary;
this.areaFillRule = fillRule;
this.areaFill = true;
this.fillColor = this.curColor;
this.areaPointsX.clear();
this.areaPointsY.clear();
this.areaPolygons.clear();
this.currentPolyPts = 0;
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillColor, patternSet, pattern));
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillRule=%d fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillRule, fillColor, patternSet, pattern));
}
private void endArea() {
@@ -928,11 +1076,11 @@ public class GocaDecoder {
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",
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix));
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, areaFillRule));
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;
areaPointsX.clear();
areaPointsY.clear();
@@ -971,11 +1119,17 @@ public class GocaDecoder {
}
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.imgX = x;
this.imgY = y;
this.imgWidth = w;
this.imgHeight = h;
this.imgBitDepth = bitDepth;
this.imgCompression = compression;
this.imgBuffer.clear();
}
@@ -991,7 +1145,7 @@ public class GocaDecoder {
int px = plane.mapX(imgX);
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;
imgBuffer.clear();
}
@@ -1328,7 +1482,7 @@ public class GocaDecoder {
}
String text = new String(chars);
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) {
for (int i = 0; i < textLen; i++) {
int code = data[pos + i] & 0xFF;
@@ -1366,7 +1520,7 @@ public class GocaDecoder {
}
String text = new String(chars);
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle);
curColor, cw, ch, charDir, charAngle, charShear);
}
switch (charDir) {
@@ -1395,11 +1549,15 @@ public class GocaDecoder {
* 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) {
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 (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 {
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.
*/
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;
char[] chars = new char[length];
for (int i = 0; i < length; 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;
}
/**
* 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.canvasHeight = h;
this.rgbBuffer = newBuffer;
updateViewingWindowPixels();
}
public synchronized void clear() {
@@ -170,6 +171,59 @@ public class GraphicsPlane {
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() {
return canvasWidth;
}
@@ -313,6 +367,11 @@ public class GraphicsPlane {
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
*/
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) {
int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return;
@@ -447,11 +506,15 @@ public class GraphicsPlane {
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
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 + 1, y, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
for (int dx = -extra; dx <= extra; dx++) {
for (int dy = -extra; dy <= extra; dy++) {
if (dx == 0 && dy == 0) continue;
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
}
}
} else {
// Perceptual gamma correction for crisp contrast on dark backgrounds
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);
}
/**
* 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,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
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;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
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) &&
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
// Find polygon vertical bounds across all points
int minY = py[0];
int maxY = py[0];
for (int i = 1; i < numPoints; i++) {
@@ -676,7 +758,7 @@ public class GraphicsPlane {
minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>();
List<NodeInter> nodeIntersections = new ArrayList<>();
byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && programSymbolManager != null) {
@@ -691,7 +773,7 @@ public class GraphicsPlane {
}
for (int y = minY; y <= maxY; y++) {
nodeX.clear();
nodeIntersections.clear();
int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) {
@@ -705,7 +787,8 @@ public class GraphicsPlane {
int xj = px[offset + j];
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
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;
}
@@ -713,35 +796,72 @@ public class GraphicsPlane {
offset += pLen;
}
Collections.sort(nodeX);
Collections.sort(nodeIntersections);
for (int i = 0; i < nodeX.size(); i += 2) {
if (i + 1 >= nodeX.size()) break;
int leftX = Math.max(0, nodeX.get(i));
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
int winding = 0;
for (int i = 0; i < nodeIntersections.size() - 1; i++) {
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++) {
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);
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);
} 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 {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
}
}
} else {
for (int i = 0; i < nodeIntersections.size(); i += 2) {
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);
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background)
setPixel(x, y, bg);
} 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);
}
}
}
}
@@ -857,19 +977,29 @@ public class GraphicsPlane {
*/
public synchronized void drawText(double x, double y, String text, int colorArgb,
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 (textRenderer != null) {
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
hasContent = true;
updateCount++;
} 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,
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,
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;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
@@ -884,25 +1019,37 @@ public class GraphicsPlane {
double ch = cellHeight > 0 ? cellHeight : 20.0;
double curX = x;
double curY = y;
if (dir == GocaConstants.CD_TB) {
curY += ch;
} else if (dir == GocaConstants.CD_RL) {
curX -= cw;
double radAngle = Math.toRadians(angle);
double cosA = Math.cos(radAngle);
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++) {
char c = text.charAt(i);
drawVssChar(curX, curY, c, color, cw, ch);
drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
switch (dir) {
case GocaConstants.CD_TB: curY += ch; break;
case GocaConstants.CD_RL: curX -= cw; break;
case GocaConstants.CD_BT: curY -= ch; break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
if (angle != 0.0) {
curX += cw * cosA;
curY += cw * sinA;
} else {
switch (dir) {
case GocaConstants.CD_TB: curY += ch; break;
case GocaConstants.CD_RL: curX -= cw; break;
case GocaConstants.CD_BT: curY -= ch; break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
}
}
}
hasContent = true;
@@ -911,10 +1058,19 @@ public class GraphicsPlane {
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
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) {
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;
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
return;
@@ -924,6 +1080,11 @@ public class GraphicsPlane {
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;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
@@ -941,8 +1102,18 @@ public class GraphicsPlane {
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 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]);
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) {
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;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
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 < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
byte[] rawData = imageData;
if (compressionMode == GocaConstants.IMG_RLE) {
rawData = decompressGocaRle(imageData, width, height, bitDepth);
}
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
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;
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 codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int bytesPerSymbol;
if (loadFormat == 1) {
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
} else {
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
}
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (sliceBytes <= 0) sliceBytes = 18;
if (bytesPerSymbol <= 0) {
bytesPerSymbol = 18;
int bytesPerSymbol;
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
bytesPerSymbol = sliceBytes * 3;
} else {
bytesPerSymbol = sliceBytes;
}
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));
}
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
// 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 {
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));
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);
}
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,7 @@ public class PD3270 {
private static final Logger log = Logger.getLogger(PD3270.class.getName());
private final PrinterConfig config;
private PrinterDefinitionTable pdt;
private String destination;
private boolean open = false;
@@ -38,6 +39,7 @@ public class PD3270 {
public PD3270(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig();
this.pdt = this.config.getPrinterDefinitionTable();
this.destination = this.config.getDestinationTarget();
}
@@ -118,6 +120,13 @@ public class PD3270 {
if (!open) {
openPrinter(destination);
}
if (pdt != null) {
byte[] translated = pdt.translateChar(c);
if (translated != null && translated.length > 0) {
writePrintBytes(translated, 0, translated.length);
return;
}
}
try {
byte[] b = String.valueOf(c).getBytes(outputCharset);
memoryStream.write(b);
@@ -141,6 +150,12 @@ public class PD3270 {
if (!open) {
openPrinter(destination);
}
if (pdt != null) {
for (int i = 0; i < s.length(); i++) {
writePrintChar(s.charAt(i));
}
return;
}
try {
byte[] b = s.getBytes(outputCharset);
memoryStream.write(b);
@@ -168,10 +183,149 @@ public class PD3270 {
*/
public synchronized void formFeed() {
pageCount++;
writePrintChar('\f');
if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_PAGE_FEED)) {
writeControlCode(PrinterDefinitionTable.CMD_PAGE_FEED);
} else {
writePrintChar('\f');
}
flush();
}
/**
* Send raw PDT control sequence if defined.
*/
public synchronized void writeControlCode(String commandName) {
if (pdt != null && commandName != null) {
byte[] seq = pdt.getControlCode(commandName);
if (seq != null && seq.length > 0) {
writePrintBytes(seq, 0, seq.length);
}
}
}
/**
* Initialize / start print job using PDT if available.
*/
public synchronized void startJob() {
if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_START_JOB)) {
writeControlCode(PrinterDefinitionTable.CMD_START_JOB);
}
}
/**
* End / finish print job using PDT if available.
*/
public synchronized void endJob() {
if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_END_JOB)) {
writeControlCode(PrinterDefinitionTable.CMD_END_JOB);
}
flush();
}
/**
* Toggle bold / emphasized printing.
*/
public synchronized void setBold(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_BOLD : PrinterDefinitionTable.CMD_END_BOLD);
}
/**
* Toggle underline printing.
*/
public synchronized void setUnderline(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_UNDERLINE : PrinterDefinitionTable.CMD_END_UNDERLINE);
}
/**
* Toggle italic printing.
*/
public synchronized void setItalic(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_ITALIC : PrinterDefinitionTable.CMD_END_ITALIC);
}
/**
* Toggle double-strike printing.
*/
public synchronized void setDoubleStrike(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_DOUBLE_STRIKE : PrinterDefinitionTable.CMD_END_DOUBLE_STRIKE);
}
/**
* Toggle double-width character expansion.
*/
public synchronized void setDoubleWidth(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_DOUBLE_WIDTH : PrinterDefinitionTable.CMD_END_DOUBLE_WIDTH);
}
/**
* Toggle subscript printing.
*/
public synchronized void setSubscript(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_SUBSCRIPT : PrinterDefinitionTable.CMD_END_SUBSCRIPT);
}
/**
* Toggle superscript printing.
*/
public synchronized void setSuperscript(boolean enable) {
writeControlCode(enable ? PrinterDefinitionTable.CMD_START_SUPERSCRIPT : PrinterDefinitionTable.CMD_END_SUPERSCRIPT);
}
/**
* Set Characters Per Inch (CPI) via PDT.
*/
public synchronized void setCPI(int cpi) {
switch (cpi) {
case 10: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_10); break;
case 12: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_12); break;
case 15: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_15); break;
case 17: writeControlCode(PrinterDefinitionTable.CMD_SET_CPI_17); break;
default: break;
}
}
/**
* Set Lines Per Inch (LPI) via PDT.
*/
public synchronized void setLPI(int lpi) {
switch (lpi) {
case 6: writeControlCode(PrinterDefinitionTable.CMD_SET_LPI_6); break;
case 8: writeControlCode(PrinterDefinitionTable.CMD_SET_LPI_8); break;
default: break;
}
}
/**
* Process GDDM host print escape sequence or raw host passthrough.
*/
public synchronized void processGddmEscape(byte[] escapeSeq, int offset, int length) {
if (escapeSeq == null || length <= 0 || offset < 0 || offset + length > escapeSeq.length) {
return;
}
writePrintBytes(escapeSeq, offset, length);
}
/**
* High-level print string method.
*/
public synchronized void print(String text) {
writePrintString(text);
}
/**
* High-level print byte array method.
*/
public synchronized void print(byte[] data, int offset, int length) {
writePrintBytes(data, offset, length);
}
/**
* High-level print single character method.
*/
public synchronized void print(char c) {
writePrintChar(c);
}
/**
* Flush all buffered print data to target stream.
*/
@@ -270,4 +424,12 @@ public class PD3270 {
this.outputCharset = charset;
}
}
public PrinterDefinitionTable getPDT() {
return pdt;
}
public void setPDT(PrinterDefinitionTable pdt) {
this.pdt = pdt != null ? pdt : PrinterDefinitionTable.createPlainTextPDT();
}
}
@@ -35,6 +35,10 @@ public class PrintPS3270 {
private int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL;
// Auto-Flush Timer (Phase 7)
private java.util.Timer autoFlushTimer;
private final Object timerLock = new Object();
public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config);
@@ -251,7 +255,48 @@ public class PrintPS3270 {
}
if (startPrint) {
cancelAutoFlush();
flushPrintBuffer();
} else {
scheduleAutoFlush();
}
}
public synchronized void processPrintComplete() {
cancelAutoFlush();
flushPrintBuffer();
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
pd.endJob();
}
public void scheduleAutoFlush() {
long timeout = config.getAutoFlushTimeoutMs();
if (timeout <= 0) return;
synchronized (timerLock) {
cancelAutoFlush();
autoFlushTimer = new java.util.Timer("PrintPS3270-AutoFlush", true);
autoFlushTimer.schedule(new java.util.TimerTask() {
@Override
public void run() {
synchronized (PrintPS3270.this) {
flushPrintBuffer();
}
}
}, timeout);
}
}
public void cancelAutoFlush() {
synchronized (timerLock) {
if (autoFlushTimer != null) {
autoFlushTimer.cancel();
autoFlushTimer = null;
}
}
}
@@ -81,6 +81,26 @@ public class PrintSCS3270 {
this.lineModified = false;
}
/**
* Process host data stream containing SCS orders and characters.
* @param data Byte array from host.
*/
public synchronized void processRecord(byte[] data) {
if (data != null) {
processRecord(data, 0, data.length);
}
}
/**
* Process host data stream containing SCS orders and characters.
* @param data Byte array from host.
* @param offset Starting offset.
* @param length Number of bytes to process.
*/
public synchronized void processRecord(byte[] data, int offset, int length) {
processHostData(data, offset, length);
}
/**
* Process host data stream containing SCS orders and characters.
* @param data Byte array from host.
@@ -108,8 +128,6 @@ public class PrintSCS3270 {
int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0;
int orderTotalLen = 2 + (paramLen > 0 ? (paramLen + 1) : 1); // 0x2B + SubOrder + paramLen + payload
// In standard SCS, paramLen byte specifies length of following parameters
// or paramLen is total length including length byte.
int bytesAvailable = end - idx;
int sliceLen = Math.min(orderTotalLen, bytesAvailable);
@@ -126,6 +144,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_STO:
processSetTextOrientation(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SCS:
processSelectCharacterSet(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SEAC:
processSetEnhancedAttribute(data, idx, sliceLen);
break;
@@ -135,6 +156,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_PPV:
processPresentationPositionVertical(data, idx, sliceLen);
break;
case PrinterConstants.SCS_GEA:
processSetGraphicErrorAction(data, idx, sliceLen);
break;
default:
log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder));
break;
@@ -154,12 +178,12 @@ public class PrintSCS3270 {
continue;
}
// Check TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRS) {
// Check TRN / TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRN) {
if (idx + 1 < end) {
int trsLen = data[idx + 1] & 0xFF;
int actualTrs = Math.min(trsLen, end - (idx + 2));
processTransparentStream(data, idx + 2, actualTrs);
processTRN(data, idx + 2, actualTrs);
idx += 2 + actualTrs;
} else {
idx = end;
@@ -167,52 +191,83 @@ public class PrintSCS3270 {
continue;
}
// Check Single-Byte SCS Controls
// Check Single-Byte SCS Controls (Full 28-command set)
switch (b) {
case PrinterConstants.SCS_NUL:
// Null - ignored
case PrinterConstants.SCS_NOP:
// Null / No-op - ignored
idx++;
break;
case PrinterConstants.SCS_VCS:
if (idx + 1 < end) {
int chan = data[idx + 1] & 0xFF;
processVCS(chan);
idx += 2;
} else {
idx++;
}
break;
case PrinterConstants.SCS_CR:
carriageReturn();
case PrinterConstants.SCS_RCR:
processCR();
idx++;
break;
case PrinterConstants.SCS_LF:
lineFeed();
processLF();
idx++;
break;
case PrinterConstants.SCS_NL:
case PrinterConstants.SCS_RNLS:
newLine();
processNL();
idx++;
break;
case PrinterConstants.SCS_FF:
formFeed();
processFF();
idx++;
break;
case PrinterConstants.SCS_BS:
case PrinterConstants.SCS_NBS:
backspace();
processBS();
idx++;
break;
case PrinterConstants.SCS_HT:
processHorizontalTab();
processHT();
idx++;
break;
case PrinterConstants.SCS_VT:
processVerticalTab();
processVT();
idx++;
break;
case PrinterConstants.SCS_SO:
processSO();
idx++;
break;
case PrinterConstants.SCS_SI:
processSI();
idx++;
break;
case PrinterConstants.SCS_ENP:
presentationEnabled = true;
processENP();
idx++;
break;
case PrinterConstants.SCS_INP:
presentationEnabled = false;
processINP();
idx++;
break;
case PrinterConstants.SCS_POC:
processPOC(null);
idx++;
break;
case PrinterConstants.SCS_BEL:
// Sound alarm
processBEL();
idx++;
break;
case PrinterConstants.SCS_IRS:
processIRS();
idx++;
break;
case PrinterConstants.SCS_SUB:
processSUB();
idx++;
break;
case PrinterConstants.SCS_GE:
@@ -233,7 +288,17 @@ public class PrintSCS3270 {
// Standard printable character
if (presentationEnabled) {
char ch = translator.ebcdicToUnicode(b);
printCharacter(ch);
if (ch == 0 || ch == '\uFFFD') {
int gea = config.getGraphicErrorAction();
if (gea == PrinterConstants.GEA_SUBSTITUTE_SPECIFIED) {
ch = config.getGraphicErrorReplacementChar();
printCharacter(ch);
} else if (gea != PrinterConstants.GEA_INHIBIT_INVALID) {
printCharacter(ch != 0 ? ch : '?');
}
} else {
printCharacter(ch);
}
}
idx++;
break;
@@ -293,10 +358,22 @@ public class PrintSCS3270 {
// ========== SCS Order Implementations ==========
public synchronized void carriageReturn() {
processCR();
}
public synchronized void processCR() {
currentCol = leftMargin;
}
public synchronized void processRCR() {
processCR();
}
public synchronized void lineFeed() {
processLF();
}
public synchronized void processLF() {
flushLineBuffer();
currentRow++;
if (currentRow > bottomMargin || currentRow > mpl) {
@@ -305,11 +382,23 @@ public class PrintSCS3270 {
}
public synchronized void newLine() {
carriageReturn();
lineFeed();
processNL();
}
public synchronized void processNL() {
processCR();
processLF();
}
public synchronized void processRNL() {
processNL();
}
public synchronized void formFeed() {
processFF();
}
public synchronized void processFF() {
flushLineBuffer();
pd.formFeed();
currentRow = topMargin;
@@ -317,18 +406,139 @@ public class PrintSCS3270 {
}
public synchronized void backspace() {
processBS();
}
public synchronized void processBS() {
if (currentCol > leftMargin) {
currentCol -= (doubleWidth ? 2 : 1);
if (currentCol < leftMargin) currentCol = leftMargin;
}
}
public synchronized void processNBS() {
processBS();
}
public synchronized void processIRS() {
// SCS Index Return (0x33): Advance one line down and return to left margin
processNL();
}
public synchronized void processVCS(int channel) {
int targetLine = config.getChannelLine(channel);
if (targetLine > 0) {
if (targetLine < currentRow) {
processFF();
}
while (currentRow < targetLine && currentRow < bottomMargin) {
processLF();
}
}
}
public synchronized void processTRN(byte[] rawBytes) {
if (rawBytes != null && rawBytes.length > 0) {
processTRN(rawBytes, 0, rawBytes.length);
}
}
public synchronized void processTRN(byte[] data, int offset, int len) {
if (data != null && len > 0) {
pd.writePrintBytes(data, offset, len);
}
}
public synchronized void processTransparentStream(byte[] data, int offset, int len) {
processTRN(data, offset, len);
}
public synchronized void processGEA(int action) {
this.config.setGraphicErrorAction(action);
}
public synchronized void processGEA(int action, int replacementChar) {
this.config.setGraphicErrorAction(action);
if (replacementChar > 0) {
this.config.setGraphicErrorReplacementChar((char) replacementChar);
}
}
public synchronized void processSO() {
// Shift-Out (DBCS mode toggle)
}
public synchronized void processSI() {
// Shift-In (SBCS mode toggle)
}
public synchronized void processENP() {
presentationEnabled = true;
}
public synchronized void processINP() {
presentationEnabled = false;
}
public synchronized void processBEL() {
log.fine("SCS BEL (Sound Alarm)");
}
public synchronized void processSUB() {
int gea = config.getGraphicErrorAction();
if (gea == PrinterConstants.GEA_SUBSTITUTE_SPECIFIED) {
printCharacter(config.getGraphicErrorReplacementChar());
} else if (gea != PrinterConstants.GEA_INHIBIT_INVALID) {
printCharacter('?');
}
}
public synchronized void processPOC(byte[] msg) {
log.fine("SCS Program Operator Communication");
}
public synchronized void processSCS(int charset) {
log.fine("SCS Select Character Set: " + charset);
}
public synchronized void processHT() {
processHorizontalTab();
}
public synchronized void processVT() {
processVerticalTab();
}
public synchronized void processPP(byte[] data, int offset, int len) {
processPresentationPositionAdvancing(data, offset, len);
}
public synchronized void processSHF(byte[] data, int offset, int len) {
processSetHorizontalFormat(data, offset, len);
}
public synchronized void processSVF(byte[] data, int offset, int len) {
processSetVerticalFormat(data, offset, len);
}
public synchronized void processSLD(byte[] data, int offset, int len) {
processSetLineDensity(data, offset, len);
}
public synchronized void processSTO(byte[] data, int offset, int len) {
processSetTextOrientation(data, offset, len);
}
public synchronized void processSA(byte[] data, int offset, int len) {
processSetAttribute(data, offset, len);
}
public synchronized void advanceToNextLine() {
newLine();
processNL();
}
public synchronized void advanceToNextPage() {
formFeed();
processFF();
}
// ========== Tab Stops and Calculations ==========
@@ -370,7 +580,7 @@ public class PrintSCS3270 {
int nextTab = calculateVerticalTab(currentRow);
if (nextTab > 0 && nextTab <= bottomMargin) {
while (currentRow < nextTab) {
lineFeed();
processLF();
}
} else {
advanceToNextPage();
@@ -414,20 +624,59 @@ public class PrintSCS3270 {
}
public synchronized void setPrintDensity(int cpi, int lpi) {
if (cpi > 0) this.cpi = cpi;
if (lpi > 0) this.lpi = lpi;
if (cpi > 0) {
this.cpi = cpi;
if (pd != null) pd.setCPI(cpi);
}
if (lpi > 0) {
this.lpi = lpi;
if (pd != null) pd.setLPI(lpi);
}
}
public synchronized void setEnhancedHighlight(int highlightType) {
this.activeHighlight = highlightType;
if (pd != null) {
switch (highlightType) {
case PrinterConstants.SEAC_EMPHASIZED:
pd.setBold(true);
break;
case PrinterConstants.SEAC_ITALIC:
pd.setItalic(true);
break;
case PrinterConstants.SEAC_UNDERLINE:
pd.setUnderline(true);
break;
case PrinterConstants.SEAC_DOUBLE_STRIKE:
pd.setDoubleStrike(true);
break;
case PrinterConstants.SEAC_SUPERSCRIPT:
pd.setSuperscript(true);
break;
case PrinterConstants.SEAC_SUBSCRIPT:
pd.setSubscript(true);
break;
case PrinterConstants.SEAC_DEFAULT:
default:
pd.setBold(false);
pd.setItalic(false);
pd.setUnderline(false);
pd.setDoubleStrike(false);
pd.setSubscript(false);
pd.setSuperscript(false);
break;
}
}
}
public synchronized void startDoubleWidthCharacters() {
this.doubleWidth = true;
if (pd != null) pd.setDoubleWidth(true);
}
public synchronized void endDoubleWidthCharacters() {
this.doubleWidth = false;
if (pd != null) pd.setDoubleWidth(false);
}
public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) {
@@ -484,7 +733,8 @@ public class PrintSCS3270 {
if (points > 0) {
// Line density in points / inch (72 points = 1 inch)
// 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI
this.lpi = Math.max(1, 72 / points);
int computedLpi = Math.max(1, 72 / points);
setPrintDensity(this.cpi, computedLpi);
}
}
@@ -494,6 +744,21 @@ public class PrintSCS3270 {
}
}
public synchronized void processSelectCharacterSet(byte[] data, int offset, int len) {
if (len >= 4) {
int cs = data[offset + 3] & 0xFF;
processSCS(cs);
}
}
public synchronized void processSetGraphicErrorAction(byte[] data, int offset, int len) {
if (len >= 4) {
int action = data[offset + 3] & 0xFF;
int repChar = (len >= 5) ? (data[offset + 4] & 0xFF) : 0;
processGEA(action, repChar);
}
}
public synchronized void processSetEnhancedAttribute(byte[] data, int offset, int len) {
if (len >= 5) {
int attrVal = data[offset + 4] & 0xFF;
@@ -520,11 +785,11 @@ public class PrintSCS3270 {
if (subfn == PrinterConstants.POS_ABSOLUTE) {
while (currentRow < val && currentRow < bottomMargin) {
lineFeed();
processLF();
}
} else if (subfn == PrinterConstants.POS_RELATIVE) {
for (int i = 0; i < val && currentRow < bottomMargin; i++) {
lineFeed();
processLF();
}
}
}
@@ -536,19 +801,23 @@ public class PrintSCS3270 {
if (attrType == PrinterConstants.SA_COLOR) {
this.activeColor = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE) {
this.activeHighlight = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE || attrType == PrinterConstants.SA_EXT_HILITE) {
setEnhancedHighlight(attrVal);
} else if (attrType == PrinterConstants.SA_CHARSET) {
processSCS(attrVal);
} else if (attrType == PrinterConstants.SA_RESET) {
this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
this.doubleWidth = false;
setEnhancedHighlight(PrinterConstants.SEAC_DEFAULT);
endDoubleWidthCharacters();
}
}
public synchronized void processTransparentStream(byte[] data, int offset, int len) {
if (len > 0) {
pd.writePrintBytes(data, offset, len);
}
public int getChannelLine(int channel) {
return config.getChannelLine(channel);
}
public void setChannelLine(int channel, int line) {
config.setChannelLine(channel, line);
}
// ========== Accessors ==========
@@ -571,13 +840,16 @@ public class PrintSCS3270 {
public int getCharsPerInch() { return cpi; }
public boolean isDoubleWidth() { return doubleWidth; }
public void setDoubleWidth(boolean dw) { this.doubleWidth = dw; }
public void setDoubleWidth(boolean dw) {
if (dw) startDoubleWidthCharacters();
else endDoubleWidthCharacters();
}
public int getActiveColor() { return activeColor; }
public void setActiveColor(int color) { this.activeColor = color; }
public int getActiveHighlight() { return activeHighlight; }
public void setActiveHighlight(int hilite) { this.activeHighlight = hilite; }
public void setActiveHighlight(int hilite) { setEnhancedHighlight(hilite); }
public int getTextOrientation() { return textOrientation; }
@@ -48,26 +48,44 @@ public class PrinterConfig {
private boolean autoFlushOnEoj = true;
private boolean autoReconnect = false;
public PrinterConfig() {}
// Phase 7 Enhancements: PDT, Auto-Flush Timer, Channel Tapes, GEA
private PrinterDefinitionTable printerDefinitionTable = PrinterDefinitionTable.createPlainTextPDT();
private final int[] channelTable = new int[13]; // 1-based index (channels 1..12)
private long autoFlushTimeoutMs = 2000;
private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION;
private char graphicErrorReplacementChar = '?';
public PrinterConfig() {
initDefaultChannelTable();
}
public PrinterConfig(String host, int port) {
this();
this.host = host;
this.port = port;
}
public PrinterConfig(String host, int port, String printerLuName) {
this.host = host;
this.port = port;
this(host, port);
this.printerLuName = printerLuName;
}
public PrinterConfig(String host, int port, String printerLuName, boolean useTls) {
this.host = host;
this.port = port;
this.printerLuName = printerLuName;
this(host, port, printerLuName);
this.useTls = useTls;
}
private void initDefaultChannelTable() {
// Standard 12-channel carriage tape default distribution
// Channel 1 = Top Margin (1), Channel 12 = Bottom Margin (66)
channelTable[1] = 1;
int step = Math.max(1, (mpl - topMargin) / 11);
for (int c = 2; c <= 11; c++) {
channelTable[c] = Math.min(mpl, topMargin + (c - 1) * step);
}
channelTable[12] = mpl;
}
// Getters and Setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
@@ -125,6 +143,7 @@ public class PrinterConfig {
public void setMpl(int mpl) {
this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl;
initDefaultChannelTable();
}
public int getLeftMargin() { return leftMargin; }
@@ -159,4 +178,53 @@ public class PrinterConfig {
public boolean isAutoReconnect() { return autoReconnect; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
public PrinterDefinitionTable getPrinterDefinitionTable() {
return printerDefinitionTable;
}
public void setPrinterDefinitionTable(PrinterDefinitionTable printerDefinitionTable) {
this.printerDefinitionTable = printerDefinitionTable != null ? printerDefinitionTable : PrinterDefinitionTable.createPlainTextPDT();
}
public long getAutoFlushTimeoutMs() {
return autoFlushTimeoutMs;
}
public void setAutoFlushTimeoutMs(long autoFlushTimeoutMs) {
this.autoFlushTimeoutMs = autoFlushTimeoutMs;
}
public int getGraphicErrorAction() {
return graphicErrorAction;
}
public void setGraphicErrorAction(int graphicErrorAction) {
this.graphicErrorAction = graphicErrorAction;
}
public char getGraphicErrorReplacementChar() {
return graphicErrorReplacementChar;
}
public void setGraphicErrorReplacementChar(char graphicErrorReplacementChar) {
this.graphicErrorReplacementChar = graphicErrorReplacementChar;
}
public int getChannelLine(int channel) {
if (channel >= 1 && channel <= 12) {
return channelTable[channel];
}
return 1;
}
public void setChannelLine(int channel, int line) {
if (channel >= 1 && channel <= 12) {
channelTable[channel] = Math.max(1, Math.min(mpl, line));
}
}
public int[] getChannelTable() {
return channelTable.clone();
}
}
@@ -22,13 +22,18 @@ public final class PrinterConstants {
// ========== SCS Single-Byte Control Codes (EBCDIC) ==========
public static final int SCS_NUL = 0x00; // Null
public static final int SCS_NOP = 0x03; // No Operation / NB
public static final int SCS_VCS = 0x04; // Vertical Channel Select (0x04 <chan>)
public static final int SCS_HT = 0x05; // Horizontal Tab
public static final int SCS_RNLS = 0x06; // Required New Line
public static final int SCS_RNL = 0x06; // Required New Line alias
public static final int SCS_RCR = 0x07; // Required Carriage Return
public static final int SCS_GE = 0x08; // Graphic Escape
public static final int SCS_VT = 0x0B; // Vertical Tab
public static final int SCS_FF = 0x0C; // Form Feed
public static final int SCS_CR = 0x0D; // Carriage Return
public static final int SCS_SO = 0x0E; // Shift Out (DBCS mode)
public static final int SCS_SI = 0x0F; // Shift In (SBCS mode)
public static final int SCS_ENP = 0x14; // Enable Presentation
public static final int SCS_NL = 0x15; // New Line
public static final int SCS_BS = 0x16; // Backspace
@@ -36,8 +41,11 @@ public final class PrinterConstants {
public static final int SCS_INP = 0x24; // Inhibit Presentation
public static final int SCS_LF = 0x25; // Line Feed
public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm
public static final int SCS_TRS = 0x35; // Transparent Stream (0x35 <len> <bytes>)
public static final int SCS_IRS = 0x33; // Index Return
public static final int SCS_TRN = 0x35; // Transparent Stream (0x35 <len> <bytes>)
public static final int SCS_TRS = 0x35; // Transparent Stream alias
public static final int SCS_NBS = 0x36; // Numeric Backspace
public static final int SCS_SUB = 0x3F; // Substitute Character
public static final int SCS_SP = 0x40; // Space
public static final int SCS_RSP = 0x41; // Required Space
@@ -46,26 +54,39 @@ public final class PrinterConstants {
public static final int SCS_SA = 0x28; // Set Attribute (0x28 <type> <val>)
// 0x2B Sub-orders
public static final int SCS_PPV = 0xC4; // Presentation Position Vertical
public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal)
public static final int SCS_GEA = 0xC8; // Set Graphic Error Action (0x2B 0xC8)
public static final int SCS_SHF = 0xD1; // Set Horizontal Format
public static final int SCS_SVF = 0xD2; // Set Vertical Format
public static final int SCS_STO = 0xD3; // Set Text Orientation
public static final int SCS_SCS = 0xD4; // Select Character Set
public static final int SCS_SEAC = 0xD5; // Set Enhanced Attribute / Highlight
public static final int SCS_SLD = 0xD6; // Set Line Density
public static final int SCS_PPV = 0xC4; // Presentation Position Vertical
public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal)
// SA Attribute Types
public static final int SA_RESET = 0x00;
public static final int SA_HILITE = 0x41;
public static final int SA_COLOR = 0x42;
public static final int SA_CHARSET = 0x43;
public static final int SA_EXT_HILITE = 0x45;
// SEAC Highlight Values
public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_BLINK = 0xF1;
public static final int SEAC_REVERSE = 0xF2;
public static final int SEAC_UNDERLINE = 0xF4;
// SEAC / Extended Highlight Values
public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_NORMAL = 0x00;
public static final int SEAC_BLINK = 0xF1;
public static final int SEAC_REVERSE = 0xF2;
public static final int SEAC_UNDERLINE = 0xF4;
public static final int SEAC_ITALIC = 0xF8;
public static final int SEAC_EMPHASIZED = 0xF9; // Bold / Emphasized
public static final int SEAC_DOUBLE_STRIKE = 0xFA;
public static final int SEAC_SUPERSCRIPT = 0xFB;
public static final int SEAC_SUBSCRIPT = 0xFC;
// Set Graphic Error Action (GEA) Constants
public static final int GEA_NO_SUBSTITUTION = 0x00; // Stop on error or use default
public static final int GEA_SUBSTITUTE_SPECIFIED = 0x01; // Substitute specified character
public static final int GEA_INHIBIT_INVALID = 0x02; // Inhibit printing invalid character
// PPA/PPV Positioning Types
public static final int POS_ABSOLUTE = 0x01; // Absolute position (1-based)
@@ -0,0 +1,237 @@
package haus.nightmare.lib3270j.printer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* Printer Definition Table (PDT) Processor.
* Conforms to IBM Host On-Demand v14 (com.ibm.eNetwork.ECL.tn3270p.PD3270 PDT architecture).
*
* Provides translation of SCS / LU3 formatting and character commands into target
* printer control codes (e.g. PCL5, Epson ESC/P, PostScript, or Plain Text).
*/
public class PrinterDefinitionTable {
// Control sequence keys
public static final String CMD_START_JOB = "START_JOB";
public static final String CMD_END_JOB = "END_JOB";
public static final String CMD_PAGE_FEED = "PAGE_FEED";
public static final String CMD_CARRIAGE_RETURN = "CARRIAGE_RETURN";
public static final String CMD_LINE_FEED = "LINE_FEED";
public static final String CMD_NEW_LINE = "NEW_LINE";
public static final String CMD_RESET = "RESET";
// Highlights
public static final String CMD_START_BOLD = "START_BOLD";
public static final String CMD_END_BOLD = "END_BOLD";
public static final String CMD_START_UNDERLINE = "START_UNDERLINE";
public static final String CMD_END_UNDERLINE = "END_UNDERLINE";
public static final String CMD_START_ITALIC = "START_ITALIC";
public static final String CMD_END_ITALIC = "END_ITALIC";
public static final String CMD_START_DOUBLE_STRIKE = "START_DOUBLE_STRIKE";
public static final String CMD_END_DOUBLE_STRIKE = "END_DOUBLE_STRIKE";
public static final String CMD_START_DOUBLE_WIDTH = "START_DOUBLE_WIDTH";
public static final String CMD_END_DOUBLE_WIDTH = "END_DOUBLE_WIDTH";
public static final String CMD_START_SUBSCRIPT = "START_SUBSCRIPT";
public static final String CMD_END_SUBSCRIPT = "END_SUBSCRIPT";
public static final String CMD_START_SUPERSCRIPT = "START_SUPERSCRIPT";
public static final String CMD_END_SUPERSCRIPT = "END_SUPERSCRIPT";
// Densities
public static final String CMD_SET_CPI_10 = "SET_CPI_10";
public static final String CMD_SET_CPI_12 = "SET_CPI_12";
public static final String CMD_SET_CPI_15 = "SET_CPI_15";
public static final String CMD_SET_CPI_17 = "SET_CPI_17";
public static final String CMD_SET_LPI_6 = "SET_LPI_6";
public static final String CMD_SET_LPI_8 = "SET_LPI_8";
// Colors
public static final String CMD_COLOR_BLACK = "COLOR_BLACK";
public static final String CMD_COLOR_BLUE = "COLOR_BLUE";
public static final String CMD_COLOR_RED = "COLOR_RED";
public static final String CMD_COLOR_PINK = "COLOR_PINK";
public static final String CMD_COLOR_GREEN = "COLOR_GREEN";
public static final String CMD_COLOR_TURQUOISE = "COLOR_TURQUOISE";
public static final String CMD_COLOR_YELLOW = "COLOR_YELLOW";
public static final String CMD_COLOR_WHITE = "COLOR_WHITE";
private final String name;
private final String description;
private final Map<String, byte[]> controlCodes = new HashMap<>();
private final Map<Character, byte[]> charOverrides = new HashMap<>();
private Charset defaultCharset = StandardCharsets.UTF_8;
public PrinterDefinitionTable(String name, String description) {
this.name = name != null ? name : "GENERIC_TEXT";
this.description = description != null ? description : "Generic Plain Text Printer Table";
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public Charset getDefaultCharset() {
return defaultCharset;
}
public void setDefaultCharset(Charset defaultCharset) {
if (defaultCharset != null) {
this.defaultCharset = defaultCharset;
}
}
public void setControlCode(String commandName, byte[] sequence) {
if (commandName != null && sequence != null) {
controlCodes.put(commandName, sequence.clone());
}
}
public void setControlCode(String commandName, String escapeSequence) {
if (commandName != null && escapeSequence != null) {
controlCodes.put(commandName, escapeSequence.getBytes(StandardCharsets.ISO_8859_1));
}
}
public byte[] getControlCode(String commandName) {
return controlCodes.get(commandName);
}
public boolean hasControlCode(String commandName) {
return controlCodes.containsKey(commandName);
}
public void setCharOverride(char c, byte[] sequence) {
if (sequence != null) {
charOverrides.put(c, sequence.clone());
}
}
public byte[] getCharOverride(char c) {
return charOverrides.get(c);
}
public byte[] translateChar(char c) {
byte[] override = charOverrides.get(c);
if (override != null) {
return override;
}
return String.valueOf(c).getBytes(defaultCharset);
}
// ==========================================
// Built-in Factory Presets
// ==========================================
/**
* Plain Text / Generic ASCII output definition.
*/
public static PrinterDefinitionTable createPlainTextPDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("PLAIN_TEXT", "Standard ASCII / Plain Text");
pdt.setControlCode(CMD_PAGE_FEED, "\f");
pdt.setControlCode(CMD_CARRIAGE_RETURN, "\r");
pdt.setControlCode(CMD_LINE_FEED, "\n");
pdt.setControlCode(CMD_NEW_LINE, "\r\n");
return pdt;
}
/**
* HP PCL 5 / PCL 6 Printer Definition Table.
*/
public static PrinterDefinitionTable createPcl5PDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("PCL_5", "Hewlett-Packard PCL 5 / PCL 6");
pdt.setDefaultCharset(StandardCharsets.ISO_8859_1);
// Control codes (ESC is \033)
pdt.setControlCode(CMD_START_JOB, "\033E"); // Reset / Initialize
pdt.setControlCode(CMD_END_JOB, "\033E"); // Reset
pdt.setControlCode(CMD_RESET, "\033E");
pdt.setControlCode(CMD_PAGE_FEED, "\f");
pdt.setControlCode(CMD_CARRIAGE_RETURN, "\r");
pdt.setControlCode(CMD_LINE_FEED, "\n");
pdt.setControlCode(CMD_NEW_LINE, "\r\n");
// Highlights
pdt.setControlCode(CMD_START_BOLD, "\033(s3B"); // Bold
pdt.setControlCode(CMD_END_BOLD, "\033(s0B"); // Normal stroke weight
pdt.setControlCode(CMD_START_UNDERLINE, "\033&d0D"); // Underline
pdt.setControlCode(CMD_END_UNDERLINE, "\033&d@"); // Underline off
pdt.setControlCode(CMD_START_ITALIC, "\033(s1S"); // Italic posture
pdt.setControlCode(CMD_END_ITALIC, "\033(s0S"); // Upright posture
pdt.setControlCode(CMD_START_DOUBLE_STRIKE, "\033(s3B");
pdt.setControlCode(CMD_END_DOUBLE_STRIKE, "\033(s0B");
pdt.setControlCode(CMD_START_DOUBLE_WIDTH, "\033(s0S\033&k1W");
pdt.setControlCode(CMD_END_DOUBLE_WIDTH, "\033&k0W");
// Pitch & Spacing
pdt.setControlCode(CMD_SET_CPI_10, "\033&k0S\033(s10H"); // 10 CPI
pdt.setControlCode(CMD_SET_CPI_12, "\033&k2S\033(s12H"); // 12 CPI
pdt.setControlCode(CMD_SET_CPI_15, "\033(s15H"); // 15 CPI
pdt.setControlCode(CMD_SET_CPI_17, "\033(s17.14H"); // 17 CPI
pdt.setControlCode(CMD_SET_LPI_6, "\033&l6D"); // 6 LPI
pdt.setControlCode(CMD_SET_LPI_8, "\033&l8D"); // 8 LPI
return pdt;
}
/**
* Epson ESC/P and ESC/P 2 Printer Definition Table.
*/
public static PrinterDefinitionTable createEpsonEscPPDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("EPSON_ESC_P", "Epson ESC/P & ESC/P 2");
pdt.setDefaultCharset(StandardCharsets.ISO_8859_1);
pdt.setControlCode(CMD_START_JOB, "\033@"); // ESC @ Initialize
pdt.setControlCode(CMD_END_JOB, "\033@");
pdt.setControlCode(CMD_RESET, "\033@");
pdt.setControlCode(CMD_PAGE_FEED, "\f");
pdt.setControlCode(CMD_CARRIAGE_RETURN, "\r");
pdt.setControlCode(CMD_LINE_FEED, "\n");
pdt.setControlCode(CMD_NEW_LINE, "\r\n");
// Highlights
pdt.setControlCode(CMD_START_BOLD, "\033E"); // ESC E (Emphasized/Bold on)
pdt.setControlCode(CMD_END_BOLD, "\033F"); // ESC F (Emphasized/Bold off)
pdt.setControlCode(CMD_START_UNDERLINE, "\033-1"); // ESC - 1 (Underline on)
pdt.setControlCode(CMD_END_UNDERLINE, "\033-0"); // ESC - 0 (Underline off)
pdt.setControlCode(CMD_START_ITALIC, "\0334"); // ESC 4 (Italic on)
pdt.setControlCode(CMD_END_ITALIC, "\0335"); // ESC 5 (Italic off)
pdt.setControlCode(CMD_START_DOUBLE_STRIKE, "\033G"); // ESC G (Double-strike on)
pdt.setControlCode(CMD_END_DOUBLE_STRIKE, "\033H"); // ESC H (Double-strike off)
pdt.setControlCode(CMD_START_DOUBLE_WIDTH, "\033W1"); // ESC W 1
pdt.setControlCode(CMD_END_DOUBLE_WIDTH, "\033W0"); // ESC W 0
pdt.setControlCode(CMD_START_SUPERSCRIPT, "\033S0"); // ESC S 0
pdt.setControlCode(CMD_END_SUPERSCRIPT, "\033T"); // ESC T
pdt.setControlCode(CMD_START_SUBSCRIPT, "\033S1"); // ESC S 1
pdt.setControlCode(CMD_END_SUBSCRIPT, "\033T"); // ESC T
// Pitch & Spacing
pdt.setControlCode(CMD_SET_CPI_10, "\033P"); // 10 CPI (Pica)
pdt.setControlCode(CMD_SET_CPI_12, "\033M"); // 12 CPI (Elite)
pdt.setControlCode(CMD_SET_CPI_15, "\033g"); // 15 CPI
pdt.setControlCode(CMD_SET_LPI_6, "\0332"); // 6 LPI (1/6 inch)
pdt.setControlCode(CMD_SET_LPI_8, "\0330"); // 8 LPI (1/8 inch)
return pdt;
}
/**
* Adobe PostScript Level 2/3 Printer Definition Table.
*/
public static PrinterDefinitionTable createPostScriptPDT() {
PrinterDefinitionTable pdt = new PrinterDefinitionTable("POSTSCRIPT", "Adobe PostScript Level 2/3");
pdt.setControlCode(CMD_START_JOB, "%!PS-Adobe-3.0\n/Courier findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_END_JOB, "showpage\n%%EOF\n");
pdt.setControlCode(CMD_PAGE_FEED, "showpage\n");
pdt.setControlCode(CMD_START_BOLD, "/Courier-Bold findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_END_BOLD, "/Courier findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_START_ITALIC, "/Courier-Oblique findfont 10 scalefont setfont\n");
pdt.setControlCode(CMD_END_ITALIC, "/Courier findfont 10 scalefont setfont\n");
return pdt;
}
}
@@ -166,6 +166,10 @@ public class Telnet3270EP implements Runnable {
readerThread = null;
}
if (printPs != null) {
printPs.cancelAutoFlush();
}
pd.closePrinter();
updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected");
}
@@ -260,11 +264,17 @@ public class Telnet3270EP implements Runnable {
*/
public synchronized void sendEOJ(boolean isComplete) {
log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete);
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
printPs.processPrintComplete();
} else {
scs.flushLineBuffer();
if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
pd.endJob();
}
firePrintJobComplete(pd.getPageCount(), pd.getByteCount());
@@ -98,4 +98,34 @@ public class Telnet3270EPClient {
}
}
}
public PrinterDefinitionTable getPDT() {
return protocolEngine.getPD().getPDT();
}
public void setPDT(PrinterDefinitionTable pdt) {
protocolEngine.getConfig().setPrinterDefinitionTable(pdt);
protocolEngine.getPD().setPDT(pdt);
}
public void flush() {
if (protocolEngine.getActiveLuType() == PrinterConstants.LU_TYPE_3_DS) {
protocolEngine.getPrintPS().flushPrintBuffer();
} else {
protocolEngine.getSCS().flushLineBuffer();
}
protocolEngine.getPD().flush();
}
public void sendEOJ(boolean isComplete) {
protocolEngine.sendEOJ(isComplete);
}
public int getChannelLine(int channel) {
return protocolEngine.getConfig().getChannelLine(channel);
}
public void setChannelLine(int channel, int line) {
protocolEngine.getConfig().setChannelLine(channel, line);
}
}
@@ -204,44 +204,52 @@ public final class DS3270Constants {
public static final int SF_DESTROY_PART = 0x0d;
public static final int SF_ACTIVATE_PART = 0x0e;
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_TRANSFER_DATA = 0xd0;
// ========== Query Reply codes ==========
public static final int QR_SUMMARY = 0x80; // Summary
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_TEXT_PART = 0x83; // Text 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_COLOR = 0x86; // Color Table / Alphanumeric Color
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
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_DBCS_ASIA = 0x91; // DBCS Asia
public static final int QR_DDM = 0x95; // Distributed Data Management
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
public static final int QR_FILE = 0x9f; // File Transfer
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_IMP_PART = 0xa6; // Implicit Partition Sizes
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_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_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
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_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_PORT = 0xb3; // Port Characteristics (OEM Format)
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
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)
public static final int QR_SUMMARY = 0x80; // Summary
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_TEXT_PART = 0x83; // Text 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_COLOR = 0x86; // Color Table / Alphanumeric Color
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
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_DBCS_ASIA = 0x91; // DBCS Asia
public static final int QR_DDM = 0x95; // Distributed Data Management
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
public static final int QR_FILE = 0x9f; // File Transfer
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_IMP_PART = 0xa6; // Implicit Partition Sizes
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_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_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
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_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_DEVICE = 0xb2; // Aux Device alias
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
public static final int QR_OEM_FORMAT = 0xb3; // OEM Format alias
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
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 ==========
public static final int MODEL_2_ROWS = 24;
@@ -115,4 +115,23 @@ public final class TelnetConstants {
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). */
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.
* 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.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.*;
/**
@@ -39,6 +42,14 @@ public class ScreenBuffer {
private byte defaultGr = 0x00;
private byte defaultCs = 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 Object renderLock = new Object();
@@ -216,6 +227,10 @@ public class ScreenBuffer {
this.explicitPartitionActive = (pid != 0);
}
public synchronized PartitionInfo getPartition(int pid) {
return partitions.get(pid);
}
public synchronized void eraseReset(boolean alt) {
partitions.clear();
this.activePartition = 0;
@@ -269,7 +284,7 @@ public class ScreenBuffer {
public void setFieldAttribute(int pos, byte fa) {
ExtendedAttribute ea = buffer[pos];
ea.clear();
ea.fa = fa;
ea.fa = (fa != 0) ? fa : (byte) FA_PRINTABLE;
if (!formatted) {
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
}
@@ -471,39 +486,10 @@ public class ScreenBuffer {
}
private char getAplGraphic(int ec) {
switch (ec) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
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);
if (translator != null) {
return translator.mapAPL(ec);
}
return (char) (ec & 0xFF);
}
/**
@@ -567,4 +553,608 @@ public class ScreenBuffer {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
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 boolean IsEntryAssistDOCmode() { return isEntryAssistDOCmode(); }
public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; }
public void SetEntryAssistDOCmode(boolean bl) { setEntryAssistDOCmode(bl); }
public boolean isEntryAssistWordWrap() { return wordWrap; }
public boolean IsEntryAssistWordWrap() { return isEntryAssistWordWrap(); }
public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; }
public void SetEntryAssistWordWrap(boolean bl) { setEntryAssistWordWrap(bl); }
public int getEntryAssistStartColumn() { return docStartCol; }
public int GetEntryAssistStartColumn() { return getEntryAssistStartColumn(); }
public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); }
public void SetEntryAssistStartColumn(int n) { setEntryAssistStartColumn(n); }
public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); }
public int GetEntryAssistEndColumn() { return getEntryAssistEndColumn(); }
public void setEntryAssistEndColumn(int n) { this.docEndCol = n; }
public void SetEntryAssistEndColumn(int n) { setEntryAssistEndColumn(n); }
public int[] getEntryAssistTabStops() { return tabStops; }
public int[] GetEntryAssistTabStops() { return getEntryAssistTabStops(); }
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); }
/**
* Perform Entry Assist word wrap if typing near/past end margin.
* Moves any partial word typed on the current line to the beginning of the next line (docStartCol).
*/
public synchronized boolean handleWordWrap(int curAddr, char typedChar) {
if (!docMode && !wordWrap) return false;
int size = rows * cols;
if (size <= 0) return false;
curAddr = ((curAddr % size) + size) % size;
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int endCol = getEntryAssistEndColumn();
int startCol = getEntryAssistStartColumn();
if (curCol < endCol) return false;
if (typedChar == ' ') {
int nextRow = (curRow + 1) % rows;
setCursorPosition(nextRow, startCol);
return true;
}
// Scan backwards to find the start of the current word on this row
int rowStartAddr = curRow * cols + startCol;
int scan = curAddr - 1;
while (scan >= rowStartAddr) {
ExtendedAttribute ea = getCell(scan);
if (ea.isFieldAttribute()) break;
int ec = ea.ec & 0xFF;
if (ec == 0 || ec == 0x40 || ea.ucs4 == ' ' || ea.ucs4 == 0) {
break;
}
scan--;
}
int wordStartAddr = scan + 1;
int wordStartCol = wordStartAddr % cols;
if (wordStartCol > startCol && wordStartAddr < curAddr) {
int wordLen = curAddr - wordStartAddr;
ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen];
for (int i = 0; i < wordLen; i++) {
wordCells[i] = new ExtendedAttribute();
wordCells[i].copyFrom(getCell(wordStartAddr + i));
getCell(wordStartAddr + i).clear();
}
int nextRow = (curRow + 1) % rows;
int targetAddr = nextRow * cols + startCol;
if (formatted) {
targetAddr = findNextUnprotected(targetAddr - 1);
}
for (int i = 0; i < wordLen; i++) {
int dst = (targetAddr + i) % size;
if (!getCell(dst).isFieldAttribute()) {
getCell(dst).copyFrom(wordCells[i]);
}
}
setCursorAddress((targetAddr + wordLen) % size);
screenChanged = true;
updateDisplaySnapshot();
return true;
} else {
// Word spans entire line or starts at startCol, wrap to next line
int nextRow = (curRow + 1) % rows;
int targetAddr = nextRow * cols + startCol;
if (formatted) {
targetAddr = findNextUnprotected(targetAddr - 1);
}
setCursorAddress(targetAddr);
screenChanged = true;
updateDisplaySnapshot();
return true;
}
}
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();
}
/**
* Inspect and balance Shift-Out (0x0E) and Shift-In (0x0F) markers, ensuring DBCS integrity.
*/
public synchronized void balanceSOSI() {
cleanAdjacentSISO(0);
processSOSI();
}
}

Some files were not shown because too many files have changed in this diff Show More