9 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
rudi a43969da08 Fix aid keys
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m11s
Release j3270 / Build & Publish Release (push) Successful in 1m10s
2026-08-31 00:11:30 +00:00
rudi e18d2f436f Adjust clearing 2026-08-31 00:00:13 +00:00
rudi 27976dd31f Add missing menus back 2026-08-30 23:46:29 +00:00
rudi a3c4b95379 Fix ADMDRAW targetting 2026-08-30 23:33:33 +00:00
137 changed files with 18216 additions and 1545 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ public class TestRunner {
public static void main(String[] args) { public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors( .selectors(
selectPackage("haus.nightmare.lib3270j") selectPackage("haus.nightmare")
) )
.build(); .build();
@@ -48,11 +48,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
public J3270App() { public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(new Color(10, 10, 10)); setBackground(Color.BLACK);
buildUI(); buildUI();
buildMenuBar(); buildMenuBar();
ThemeManager.addThemeChangeListener(this::onThemeChanged);
pack(); pack();
setLocationRelativeTo(null); setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400)); setMinimumSize(new Dimension(640, 400));
@@ -92,15 +94,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
statusBar = new StatusBar(); statusBar = new StatusBar();
getContentPane().setLayout(new BorderLayout()); getContentPane().setLayout(new BorderLayout());
getContentPane().setBackground(new Color(10, 10, 10)); getContentPane().setBackground(Color.BLACK);
getContentPane().add(terminalPanel, BorderLayout.CENTER); getContentPane().add(terminalPanel, BorderLayout.CENTER);
getContentPane().add(statusBar, BorderLayout.SOUTH); getContentPane().add(statusBar, BorderLayout.SOUTH);
} }
private void onThemeChanged(UITheme theme) {
buildMenuBar();
statusBar.applyTheme(theme);
ThemeManager.applyThemeToWindow(this);
terminalPanel.repaint();
}
private void buildMenuBar() { private void buildMenuBar() {
JMenuBar menuBar = new JMenuBar(); JMenuBar menuBar = new JMenuBar();
menuBar.setBackground(new Color(30, 30, 30)); ThemeManager.styleMenuBar(menuBar);
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
@@ -137,12 +145,37 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 3. View menu // 3. View menu
JMenu viewMenu = createMenu("View"); JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2))); viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2), true));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2))); viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2), true));
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> { viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
terminalPanel.setFontSize(16); terminalPanel.setFontSize(16);
terminalPanel.guardedPack(); 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(); viewMenu.addSeparator();
// CodePage Submenu // CodePage Submenu
@@ -174,8 +207,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
for (String cpItem : codePages) { for (String cpItem : codePages) {
String cpId = cpItem.split(" -")[0].trim(); String cpId = cpItem.split(" -")[0].trim();
JMenuItem cpMi = new JMenuItem(cpItem); JMenuItem cpMi = new JMenuItem(cpItem);
cpMi.setBackground(new Color(40, 40, 40)); ThemeManager.styleMenuItem(cpMi);
cpMi.setForeground(new Color(200, 200, 200));
cpMi.addActionListener(e -> changeCodePage(cpId)); cpMi.addActionListener(e -> changeCodePage(cpId));
cpMenu.add(cpMi); cpMenu.add(cpMi);
} }
@@ -185,8 +217,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JMenu gfxMenu = createMenu("Graphics Mode"); JMenu gfxMenu = createMenu("Graphics Mode");
for (GraphicsMode gm : GraphicsMode.values()) { for (GraphicsMode gm : GraphicsMode.values()) {
JMenuItem gmMi = new JMenuItem(gm.name()); JMenuItem gmMi = new JMenuItem(gm.name());
gmMi.setBackground(new Color(40, 40, 40)); ThemeManager.styleMenuItem(gmMi);
gmMi.setForeground(new Color(200, 200, 200));
gmMi.addActionListener(e -> changeGraphicsMode(gm)); gmMi.addActionListener(e -> changeGraphicsMode(gm));
gfxMenu.add(gmMi); gfxMenu.add(gmMi);
} }
@@ -203,17 +234,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.sendEnter(); client.sendEnter();
terminalPanel.repaint(); terminalPanel.repaint();
} }
})); }, true));
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> { actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
if (client != null) if (client != null)
client.sendClear(); client.sendClear();
terminalPanel.repaint(); terminalPanel.repaint();
})); }, true));
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> { actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
if (client != null) if (client != null)
client.reset(); client.reset();
terminalPanel.repaint(); terminalPanel.repaint();
})); }, true));
actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> { actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
client.eraseInput(); client.eraseInput();
@@ -241,7 +272,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, true)); }, true));
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); 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); menuBar.add(actionsMenu);
// 5. Help menu // 5. Help menu
@@ -257,14 +288,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private JMenu createMenu(String name) { private JMenu createMenu(String name) {
JMenu menu = new JMenu(name); JMenu menu = new JMenu(name);
menu.setForeground(new Color(200, 200, 200)); return ThemeManager.styleMenu(menu);
return menu;
} }
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) { private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
JMenuItem item = new JMenuItem(name); JMenuItem item = new JMenuItem(name);
item.setBackground(new Color(40, 40, 40)); ThemeManager.styleMenuItem(item);
item.setForeground(new Color(200, 200, 200));
if (mnemonic > 0) { if (mnemonic > 0) {
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier)); item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
@@ -677,25 +706,25 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"Insert — Toggle insert mode\n" + "Insert — Toggle insert mode\n" +
"Escape — Reset\n" + "Escape — Reset\n" +
"PageUp/Down — PF7/PF8\n" + "PageUp/Down — PF7/PF8\n" +
"Alt+C / Ctrl+K — Clear\n" + "Alt+C / Alt+K — Clear\n" +
"Alt+E — Erase Input\n" + "Alt+E — Erase Input\n" +
"Alt+A — Attention\n" + "Alt+A — Attention\n" +
"Alt+S — System Request\n" + "Alt+S — System Request\n" +
"Alt+Q — Cursor Select\n" + "Alt+Q — Cursor Select\n" +
"Alt+L — Toggle Light Pen\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+F — Find on Screen\n" +
"Cmd/Ctrl+G / F3— Find Next\n" + "Cmd/Ctrl+G / F3— Find Next\n" +
"Cmd/Ctrl+T — File Transfer\n" +
"Cmd/Ctrl+D — Disconnect\n" + "Cmd/Ctrl+D — Disconnect\n" +
"Cmd/Ctrl+Q — Quit\n" + "Cmd/Ctrl+Q — Quit";
"Cmd/Ctrl+=/-/0 — Font size +/-/reset";
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30)); ThemeManager.styleTextArea(area);
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp);
sp.setPreferredSize(new Dimension(380, 430)); sp.setPreferredSize(new Dimension(380, 430));
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
} }
@@ -726,9 +755,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30)); ThemeManager.styleTextArea(area);
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp);
sp.setPreferredSize(new Dimension(440, 430)); sp.setPreferredSize(new Dimension(440, 430));
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
} }
@@ -758,45 +787,47 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
Boolean cliTn3270e = null; Boolean cliTn3270e = null;
GraphicsMode cliGraphicsMode = null; GraphicsMode cliGraphicsMode = null;
String configFile = null; String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>(); java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
if ("--debug".equals(args[i]) || "-d".equals(args[i])) { String arg = args[i];
if ("-d".equals(arg) || "--debug".equals(arg)) {
debug = true; debug = true;
} else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) { } else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
cliTls = true; cliTls = true;
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) { } else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
cliNoVerifyCert = true; cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) { } else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
cliTn3270e = false; cliTn3270e = false;
} else if ("--tn3270e".equals(args[i])) { } else if ("--tn3270e".equals(arg)) {
cliTn3270e = true; cliTn3270e = true;
} else if (args[i].startsWith("--graphics=")) { } else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(args[i].substring(11)); cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) { } else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]); cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(args[i])) { } else if ("--no-graphics".equals(arg)) {
cliGraphicsMode = GraphicsMode.NONE; cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) { } else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i]; configFile = args[++i];
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} else { } else {
remainingArgs.add(args[i]); remainingArgs.add(arg);
} }
} }
Level logLevel = debug ? Level.ALL : Level.INFO; Level logLevel = debug ? Level.FINE : Level.INFO;
Logger globalRoot = Logger.getLogger(""); Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) { for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h); globalRoot.removeHandler(h);
} }
globalRoot.setLevel(Level.ALL);
java.util.logging.Filter appFilter = record -> { java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
String name = record.getLoggerName(); (record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
return name != null && (name.startsWith("haus.nightmare.lib3270j") || name.startsWith("haus.nightmare.j3270"));
};
ConsoleHandler consoleHandler = new ConsoleHandler(); ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(logLevel); consoleHandler.setLevel(Level.ALL);
consoleHandler.setFormatter(new SimpleFormatter()); consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter); consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler); globalRoot.addHandler(consoleHandler);
@@ -837,6 +868,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} catch (Exception e) { } catch (Exception e) {
log.fine("Could not set system look and feel"); log.fine("Could not set system look and feel");
} }
ThemeManager.applyUIManagerDefaults(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
ThemeManager.setTheme(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("apple.awt.application.name", "j3270"); System.setProperty("apple.awt.application.name", "j3270");
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.config; package haus.nightmare.j3270.config;
import haus.nightmare.j3270.ui.UITheme;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import java.awt.Color; import java.awt.Color;
import java.io.*; import java.io.*;
@@ -17,6 +18,15 @@ public class Settings {
AUTO_CONNECT AUTO_CONNECT
} }
public static UITheme getJavaUiTheme() {
String themeStr = prefs.get("javaUiTheme", UITheme.DARK.name());
return UITheme.fromString(themeStr);
}
public static void setJavaUiTheme(UITheme theme) {
prefs.put("javaUiTheme", (theme != null ? theme : UITheme.DARK).name());
}
public static String getFontFamily() { public static String getFontFamily() {
return prefs.get("fontFamily", "Monospaced"); return prefs.get("fontFamily", "Monospaced");
} }
@@ -229,6 +239,11 @@ public class Settings {
switch (key) { switch (key) {
case "fontFamily": setFontFamily(value); break; case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break; case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "javaUiTheme":
case "theme":
case "uiTheme":
setJavaUiTheme(UITheme.fromString(value));
break;
default: default:
log.warning("Unknown appearance key: " + key); log.warning("Unknown appearance key: " + key);
} }
@@ -322,6 +337,7 @@ public class Settings {
// [appearance] // [appearance]
w.println("[appearance]"); w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("fontFamily = " + getFontFamily()); w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize()); w.println("fontSize = " + getFontSize());
w.println(); w.println();
@@ -1,6 +1,7 @@
package haus.nightmare.j3270.ft; package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.HostDirectoryDialog; import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants; import haus.nightmare.lib3270j.ft.FTConstants;
@@ -88,6 +89,7 @@ public class FileTransferDialog extends JDialog {
// Local File // Local File
localFileField = new JTextField(20); localFileField = new JTextField(20);
browseLocalButton = new JButton("Browse..."); browseLocalButton = new JButton("Browse...");
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
browseLocalButton.addActionListener(e -> browseLocalFile()); browseLocalButton.addActionListener(e -> browseLocalFile());
JPanel localPanel = new JPanel(new BorderLayout(5, 0)); JPanel localPanel = new JPanel(new BorderLayout(5, 0));
localPanel.setOpaque(false); localPanel.setOpaque(false);
@@ -98,6 +100,7 @@ public class FileTransferDialog extends JDialog {
// Host File // Host File
hostFileField = new JTextField(20); hostFileField = new JTextField(20);
browseHostButton = new JButton("Browse Host..."); browseHostButton = new JButton("Browse Host...");
ThemeManager.styleButton(browseHostButton, ThemeManager.ButtonVariant.DEFAULT);
browseHostButton.addActionListener(e -> browseHostDirectory()); browseHostButton.addActionListener(e -> browseHostDirectory());
JPanel hostPanel = new JPanel(new BorderLayout(5, 0)); JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
hostPanel.setOpaque(false); hostPanel.setOpaque(false);
@@ -154,10 +157,7 @@ public class FileTransferDialog extends JDialog {
// Host-specific options panel (TSO dataset allocation) // Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5)); JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
hostOptsPanel.setOpaque(false); hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(BorderFactory.createTitledBorder( hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"TSO Allocation Options (Send Only)"));
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
hostOptsPanel.add(new JLabel("RECFM:")); hostOptsPanel.add(new JLabel("RECFM:"));
recfmField = new JTextField(5); recfmField = new JTextField(5);
@@ -181,10 +181,7 @@ public class FileTransferDialog extends JDialog {
// CMS options // CMS options
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0)); JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
vmOptsPanel.setOpaque(false); vmOptsPanel.setOpaque(false);
vmOptsPanel.setBorder(BorderFactory.createTitledBorder( vmOptsPanel.setBorder(ThemeManager.createTitledBorder("CMS Options"));
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"CMS Options"));
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
optionsField = new JTextField(20); optionsField = new JTextField(20);
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST); vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
@@ -200,9 +197,11 @@ public class FileTransferDialog extends JDialog {
buttonPanel.setOpaque(false); buttonPanel.setOpaque(false);
transferButton = new JButton("Start Transfer"); transferButton = new JButton("Start Transfer");
ThemeManager.styleButton(transferButton, ThemeManager.ButtonVariant.PRIMARY);
transferButton.addActionListener(e -> startTransfer()); transferButton.addActionListener(e -> startTransfer());
cancelButton = new JButton("Close"); cancelButton = new JButton("Close");
ThemeManager.styleButton(cancelButton, ThemeManager.ButtonVariant.CANCEL);
cancelButton.addActionListener(e -> dispose()); cancelButton.addActionListener(e -> dispose());
buttonPanel.add(cancelButton); buttonPanel.add(cancelButton);
@@ -212,7 +211,7 @@ public class FileTransferDialog extends JDialog {
setContentPane(mainPanel); setContentPane(mainPanel);
applyTheme(mainPanel); ThemeManager.applyThemeToWindow(this);
updateOptionStates(); updateOptionStates();
} }
@@ -34,24 +34,21 @@ public class ConnectDialog extends JDialog {
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout()); JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(5, 6, 5, 6);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14); Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
Color fg = new Color(200, 200, 200);
// Host // Host
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = 0;
JLabel hostLabel = new JLabel("Host:"); JLabel hostLabel = new JLabel("Host:");
hostLabel.setForeground(fg);
hostLabel.setFont(labelFont); hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc); mainPanel.add(hostLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
hostField = createDarkField(20); hostField = createField(20);
mainPanel.add(hostField, gbc); mainPanel.add(hostField, gbc);
// Port // Port
@@ -59,12 +56,11 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 1; gbc.gridy = 1;
gbc.weightx = 0; gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:"); JLabel portLabel = new JLabel("Port:");
portLabel.setForeground(fg);
portLabel.setFont(labelFont); portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc); mainPanel.add(portLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
portField = createDarkField(6); portField = createField(6);
portField.setText("23"); portField.setText("23");
mainPanel.add(portField, gbc); mainPanel.add(portField, gbc);
@@ -73,16 +69,14 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 2; gbc.gridy = 2;
gbc.weightx = 0; gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:"); JLabel modelLabel = new JLabel("Model:");
modelLabel.setForeground(fg);
modelLabel.setFont(labelFont); modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc); mainPanel.add(modelLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values()); modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setBackground(new Color(45, 45, 45));
modelCombo.setForeground(fg);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc); mainPanel.add(modelCombo, gbc);
// LU Name // LU Name
@@ -90,12 +84,11 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 3; gbc.gridy = 3;
gbc.weightx = 0; gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:"); JLabel luLabel = new JLabel("LU Name:");
luLabel.setForeground(fg);
luLabel.setFont(labelFont); luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc); mainPanel.add(luLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
luField = createDarkField(12); luField = createField(12);
mainPanel.add(luField, gbc); mainPanel.add(luField, gbc);
// Graphics Mode // Graphics Mode
@@ -103,16 +96,14 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 4; gbc.gridy = 4;
gbc.weightx = 0; gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:"); JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setForeground(fg);
graphicsLabel.setFont(labelFont); graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc); mainPanel.add(graphicsLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values()); graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setBackground(new Color(45, 45, 45));
graphicsCombo.setForeground(fg);
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc); mainPanel.add(graphicsCombo, gbc);
// Code Page // Code Page
@@ -120,7 +111,6 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 5; gbc.gridy = 5;
gbc.weightx = 0; gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:"); JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setForeground(fg);
cpLabel.setFont(labelFont); cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc); mainPanel.add(cpLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
@@ -139,14 +129,27 @@ public class ConnectDialog extends JDialog {
"870 - Eastern Europe / Latin-2", "870 - Eastern Europe / Latin-2",
"871 - Iceland", "871 - Iceland",
"875 - Greece (Greek)", "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)", "1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)", "1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)", "1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS", "930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS", "939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS", "935 - Simplified Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS", "937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended Mixed DBCS",
"933 - Korean Mixed DBCS" "933 - Korean Mixed DBCS"
}; };
codePageCombo = new JComboBox<>(commonCodePages); codePageCombo = new JComboBox<>(commonCodePages);
@@ -158,9 +161,8 @@ public class ConnectDialog extends JDialog {
break; break;
} }
} }
codePageCombo.setBackground(new Color(45, 45, 45));
codePageCombo.setForeground(fg);
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc); mainPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox // TLS / SSL Checkbox
@@ -168,10 +170,8 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 6; gbc.gridy = 6;
gbc.weightx = 1.0; gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL"); tlsCheckBox = new JCheckBox("Enable TLS/SSL");
tlsCheckBox.setBackground(new Color(30, 30, 30)); ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setForeground(fg);
tlsCheckBox.setFont(labelFont); tlsCheckBox.setFont(labelFont);
tlsCheckBox.setFocusPainted(false);
tlsCheckBox.addActionListener(e -> { tlsCheckBox.addActionListener(e -> {
boolean isTls = tlsCheckBox.isSelected(); boolean isTls = tlsCheckBox.isSelected();
verifyCertCheckBox.setEnabled(isTls); verifyCertCheckBox.setEnabled(isTls);
@@ -188,38 +188,31 @@ public class ConnectDialog extends JDialog {
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 7; gbc.gridy = 7;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
verifyCertCheckBox.setBackground(new Color(30, 30, 30)); ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
verifyCertCheckBox.setSelected(true); verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false); verifyCertCheckBox.setEnabled(false);
verifyCertCheckBox.setFocusPainted(false);
mainPanel.add(verifyCertCheckBox, gbc); mainPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox // TN3270E Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 8; gbc.gridy = 8;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
tn3270eCheckBox.setBackground(new Color(30, 30, 30)); ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setForeground(fg);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e()); tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
tn3270eCheckBox.setFocusPainted(false);
mainPanel.add(tn3270eCheckBox, gbc); mainPanel.add(tn3270eCheckBox, gbc);
// Buttons // Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
buttonPanel.setBackground(new Color(30, 30, 30));
JButton connectBtn = new JButton("Connect"); JButton connectBtn = new JButton("Connect");
connectBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.setForeground(Color.WHITE);
connectBtn.setFont(labelFont); connectBtn.setFont(labelFont);
connectBtn.addActionListener(e -> onConnect()); connectBtn.addActionListener(e -> onConnect());
JButton cancelBtn = new JButton("Cancel"); JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setForeground(fg);
cancelBtn.setFont(labelFont); cancelBtn.setFont(labelFont);
cancelBtn.addActionListener(e -> { cancelBtn.addActionListener(e -> {
confirmed = false; confirmed = false;
@@ -235,20 +228,16 @@ public class ConnectDialog extends JDialog {
mainPanel.add(buttonPanel, gbc); mainPanel.add(buttonPanel, gbc);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect // Enter key triggers connect
getRootPane().setDefaultButton(connectBtn); getRootPane().setDefaultButton(connectBtn);
} }
private JTextField createDarkField(int cols) { private JTextField createField(int cols) {
JTextField field = new JTextField(cols); JTextField field = new JTextField(cols);
field.setBackground(new Color(45, 45, 45));
field.setForeground(new Color(200, 200, 200));
field.setCaretColor(new Color(200, 200, 200));
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
field.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(field);
BorderFactory.createLineBorder(new Color(60, 60, 60)),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
return field; return field;
} }
@@ -28,12 +28,6 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
private JTable table; private JTable table;
private JLabel countLabel; private JLabel countLabel;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_BORDER = new Color(65, 65, 65);
private static final Color DARK_SELECTION = new Color(75, 110, 175);
public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) { public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "3270 Presentation Space Field Inspector", false); super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client; this.client = client;
@@ -61,19 +55,16 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(8, 8)); JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(DARK_BG);
// Top info bar // Top info bar
JPanel topPanel = new JPanel(new BorderLayout()); JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen"); countLabel = new JLabel("0 fields detected on screen");
countLabel.setForeground(DARK_FG);
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
topPanel.add(countLabel, BorderLayout.WEST); topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh"); JButton refreshBtn = new JButton("Refresh");
refreshBtn.setBackground(new Color(55, 55, 55)); ThemeManager.styleButton(refreshBtn, ThemeManager.ButtonVariant.DEFAULT);
refreshBtn.setForeground(DARK_FG);
refreshBtn.addActionListener(e -> refreshFields()); refreshBtn.addActionListener(e -> refreshFields());
topPanel.add(refreshBtn, BorderLayout.EAST); topPanel.add(refreshBtn, BorderLayout.EAST);
mainPanel.add(topPanel, BorderLayout.NORTH); mainPanel.add(topPanel, BorderLayout.NORTH);
@@ -91,18 +82,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
}; };
table = new JTable(tableModel); table = new JTable(tableModel);
table.setBackground(DARK_FIELD_BG); ThemeManager.styleTable(table);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(20);
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(DARK_FG);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
// Column widths // Column widths
@@ -125,8 +110,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
}); });
JScrollPane scrollPane = new JScrollPane(table); JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(DARK_BORDER)); ThemeManager.styleScrollPane(scrollPane);
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom panel // Bottom panel
@@ -134,8 +118,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
JButton jumpBtn = new JButton("Jump to Selected Field"); JButton jumpBtn = new JButton("Jump to Selected Field");
jumpBtn.setBackground(new Color(50, 100, 160)); ThemeManager.styleButton(jumpBtn, ThemeManager.ButtonVariant.PRIMARY);
jumpBtn.setForeground(Color.WHITE);
jumpBtn.addActionListener(e -> { jumpBtn.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row >= 0) { if (row >= 0) {
@@ -144,8 +127,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
}); });
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(55, 55, 55)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
bottomPanel.add(jumpBtn); bottomPanel.add(jumpBtn);
@@ -153,6 +135,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
} }
public synchronized void refreshFields() { public synchronized void refreshFields() {
@@ -42,7 +42,6 @@ public class FindDialog extends JDialog {
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
// Form // Form
JPanel formPanel = new JPanel(new GridBagLayout()); JPanel formPanel = new JPanel(new GridBagLayout());
@@ -51,14 +50,12 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Search text // Search text
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = 0;
JLabel findLabel = new JLabel("Find what:"); JLabel findLabel = new JLabel("Find what:");
findLabel.setForeground(fg);
findLabel.setFont(labelFont); findLabel.setFont(labelFont);
formPanel.add(findLabel, gbc); formPanel.add(findLabel, gbc);
@@ -66,23 +63,16 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
searchField = new JTextField(20); searchField = new JTextField(20);
searchField.setText(lastSearchText); searchField.setText(lastSearchText);
searchField.setBackground(new Color(50, 50, 50));
searchField.setForeground(fg);
searchField.setCaretColor(fg);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
searchField.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(searchField);
BorderFactory.createLineBorder(new Color(70, 70, 70)),
BorderFactory.createEmptyBorder(3, 6, 3, 6)));
formPanel.add(searchField, gbc); formPanel.add(searchField, gbc);
// Options // Options
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 1; gbc.gridy = 1;
matchCaseCheck = new JCheckBox("Match case", lastMatchCase); matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
matchCaseCheck.setForeground(fg); ThemeManager.styleCheckBox(matchCaseCheck);
matchCaseCheck.setOpaque(false);
matchCaseCheck.setFont(labelFont); matchCaseCheck.setFont(labelFont);
matchCaseCheck.setFocusPainted(false);
formPanel.add(matchCaseCheck, gbc); formPanel.add(matchCaseCheck, gbc);
// Direction // Direction
@@ -91,16 +81,11 @@ public class FindDialog extends JDialog {
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false); dirPanel.setOpaque(false);
JLabel dirLabel = new JLabel("Direction: "); JLabel dirLabel = new JLabel("Direction: ");
dirLabel.setForeground(fg);
dirLabel.setFont(labelFont); dirLabel.setFont(labelFont);
forwardRadio = new JRadioButton("Down", lastForward); forwardRadio = new JRadioButton("Down", lastForward);
backwardRadio = new JRadioButton("Up", !lastForward); backwardRadio = new JRadioButton("Up", !lastForward);
forwardRadio.setForeground(fg); ThemeManager.styleRadioButton(forwardRadio);
backwardRadio.setForeground(fg); ThemeManager.styleRadioButton(backwardRadio);
forwardRadio.setOpaque(false);
backwardRadio.setOpaque(false);
forwardRadio.setFocusPainted(false);
backwardRadio.setFocusPainted(false);
ButtonGroup bg = new ButtonGroup(); ButtonGroup bg = new ButtonGroup();
bg.add(forwardRadio); bg.add(forwardRadio);
@@ -116,7 +101,7 @@ public class FindDialog extends JDialog {
gbc.gridy = 3; gbc.gridy = 3;
gbc.gridwidth = 2; gbc.gridwidth = 2;
statusLabel = new JLabel(" "); statusLabel = new JLabel(" ");
statusLabel.setForeground(new Color(255, 120, 120)); statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
formPanel.add(statusLabel, gbc); formPanel.add(statusLabel, gbc);
@@ -127,14 +112,12 @@ public class FindDialog extends JDialog {
buttonPanel.setOpaque(false); buttonPanel.setOpaque(false);
JButton findNextBtn = new JButton("Find Next"); JButton findNextBtn = new JButton("Find Next");
findNextBtn.setBackground(new Color(60, 63, 65)); ThemeManager.styleButton(findNextBtn, ThemeManager.ButtonVariant.PRIMARY);
findNextBtn.setForeground(fg);
findNextBtn.setFont(labelFont); findNextBtn.setFont(labelFont);
findNextBtn.addActionListener(e -> findNext()); findNextBtn.addActionListener(e -> findNext());
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 63, 65)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont); closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
@@ -144,6 +127,7 @@ public class FindDialog extends JDialog {
mainPanel.add(buttonPanel, BorderLayout.SOUTH); mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
getRootPane().setDefaultButton(findNextBtn); getRootPane().setDefaultButton(findNextBtn);
searchField.addKeyListener(new KeyAdapter() { searchField.addKeyListener(new KeyAdapter() {
@@ -49,9 +49,7 @@ public class HostDirectoryDialog extends JDialog {
private void buildUI(String initialQuery) { private void buildUI(String initialQuery) {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12)); mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Top Query bar // Top Query bar
@@ -62,7 +60,6 @@ public class HostDirectoryDialog extends JDialog {
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel envLabel = new JLabel("System:"); JLabel envLabel = new JLabel("System:");
envLabel.setForeground(fg);
envLabel.setFont(font); envLabel.setFont(font);
topPanel.add(envLabel, gbc); topPanel.add(envLabel, gbc);
@@ -70,33 +67,25 @@ public class HostDirectoryDialog extends JDialog {
hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS}); hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS});
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS); if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO); else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
hostTypeCombo.setBackground(new Color(50, 50, 50)); ThemeManager.styleComboBox(hostTypeCombo);
hostTypeCombo.setForeground(fg);
topPanel.add(hostTypeCombo, gbc); topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2; gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:"); JLabel queryLabel = new JLabel("Query Pattern / Text:");
queryLabel.setForeground(fg);
queryLabel.setFont(font); queryLabel.setFont(font);
topPanel.add(queryLabel, gbc); topPanel.add(queryLabel, gbc);
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setBackground(new Color(50, 50, 50));
queryField.setForeground(fg);
queryField.setCaretColor(fg);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
queryField.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(queryField);
new LineBorder(new Color(70, 70, 70)),
new EmptyBorder(3, 5, 3, 5)));
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
gbc.gridx = 4; gbc.gridx = 4;
gbc.weightx = 0; gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse"); JButton parseBtn = new JButton("Query / Parse");
parseBtn.setBackground(new Color(55, 55, 55)); ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
parseBtn.setForeground(fg);
parseBtn.setFont(font); parseBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery()); parseBtn.addActionListener(e -> runQuery());
topPanel.add(parseBtn, gbc); topPanel.add(parseBtn, gbc);
@@ -106,18 +95,12 @@ public class HostDirectoryDialog extends JDialog {
// Table // Table
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel();
table = new JTable(tableModel); table = new JTable(tableModel);
table.setBackground(new Color(45, 45, 45)); ThemeManager.styleTable(table);
table.setForeground(fg);
table.setSelectionBackground(new Color(75, 110, 175));
table.setSelectionForeground(Color.WHITE);
table.setGridColor(new Color(65, 65, 65));
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(20);
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setBackground(new Color(50, 50, 50));
header.setForeground(fg);
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
table.addMouseListener(new MouseAdapter() { table.addMouseListener(new MouseAdapter() {
@@ -130,8 +113,7 @@ public class HostDirectoryDialog extends JDialog {
}); });
JScrollPane scrollPane = new JScrollPane(table); JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setBorder(new LineBorder(new Color(65, 65, 65))); ThemeManager.styleScrollPane(scrollPane);
scrollPane.getViewport().setBackground(new Color(45, 45, 45));
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom // Bottom
@@ -139,7 +121,7 @@ public class HostDirectoryDialog extends JDialog {
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(new Color(170, 170, 170)); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
@@ -147,20 +129,17 @@ public class HostDirectoryDialog extends JDialog {
btnPanel.setOpaque(false); btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen"); JButton pasteScreenBtn = new JButton("Parse Current Screen");
pasteScreenBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteScreenBtn.setForeground(fg);
pasteScreenBtn.setFont(font); pasteScreenBtn.setFont(font);
pasteScreenBtn.addActionListener(e -> parseCurrentScreen()); pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton selectBtn = new JButton("Select Dataset"); JButton selectBtn = new JButton("Select Dataset");
selectBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
selectBtn.setForeground(Color.WHITE);
selectBtn.setFont(font); selectBtn.setFont(font);
selectBtn.addActionListener(e -> onConfirmSelection()); selectBtn.addActionListener(e -> onConfirmSelection());
JButton cancelBtn = new JButton("Cancel"); JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setForeground(fg);
cancelBtn.setFont(font); cancelBtn.setFont(font);
cancelBtn.addActionListener(e -> { cancelBtn.addActionListener(e -> {
confirmed = false; confirmed = false;
@@ -176,6 +155,7 @@ public class HostDirectoryDialog extends JDialog {
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Initial setup // Initial setup
setupTableColumns(); setupTableColumns();
@@ -45,10 +45,6 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool Display // Spool Display
private JTextArea spoolArea; private JTextArea spoolArea;
private static final Color DARK_BG = new Color(35, 35, 35);
private static final Color DARK_FG = new Color(220, 220, 220);
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) { public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false); super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent; this.parent = parent;
@@ -65,14 +61,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(DARK_BG);
// Top: Configuration Panel // Top: Configuration Panel
JPanel topPanel = new JPanel(new GridBagLayout()); JPanel topPanel = new JPanel(new GridBagLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
topPanel.setBorder(BorderFactory.createTitledBorder( topPanel.setBorder(ThemeManager.createTitledBorder("Printer Session Configuration"));
new LineBorder(new Color(65, 65, 65)), "Printer Session Configuration"));
((TitledBorder) topPanel.getBorder()).setTitleColor(DARK_FG);
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 3, 4); gbc.insets = new Insets(3, 4, 3, 4);
@@ -83,60 +76,59 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Host / Port // Host / Port
gbc.gridx = 0; gbc.gridy = 0; gbc.gridx = 0; gbc.gridy = 0;
JLabel hLbl = new JLabel("Host:"); JLabel hLbl = new JLabel("Host:");
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont); hLbl.setFont(labelFont);
topPanel.add(hLbl, gbc); topPanel.add(hLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridx = 1; gbc.weightx = 1.0;
hostField = createDarkField(config.getHost() != null ? config.getHost() : "localhost", 14); hostField = createField(config.getHost() != null ? config.getHost() : "localhost", 14);
topPanel.add(hostField, gbc); topPanel.add(hostField, gbc);
gbc.gridx = 2; gbc.weightx = 0; gbc.gridx = 2; gbc.weightx = 0;
JLabel pLbl = new JLabel("Port:"); JLabel pLbl = new JLabel("Port:");
pLbl.setForeground(DARK_FG); pLbl.setFont(labelFont); pLbl.setFont(labelFont);
topPanel.add(pLbl, gbc); topPanel.add(pLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5; gbc.gridx = 3; gbc.weightx = 0.5;
portField = createDarkField(String.valueOf(config.getPort()), 5); portField = createField(String.valueOf(config.getPort()), 5);
topPanel.add(portField, gbc); topPanel.add(portField, gbc);
// Printer LU / Display LU // Printer LU / Display LU
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0; gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
JLabel pluLbl = new JLabel("Printer LU:"); JLabel pluLbl = new JLabel("Printer LU:");
pluLbl.setForeground(DARK_FG); pluLbl.setFont(labelFont); pluLbl.setFont(labelFont);
topPanel.add(pluLbl, gbc); topPanel.add(pluLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridx = 1; gbc.weightx = 1.0;
printerLuField = createDarkField("", 10); printerLuField = createField("", 10);
topPanel.add(printerLuField, gbc); topPanel.add(printerLuField, gbc);
gbc.gridx = 2; gbc.weightx = 0; gbc.gridx = 2; gbc.weightx = 0;
JLabel assocLbl = new JLabel("Assoc LU:"); JLabel assocLbl = new JLabel("Assoc LU:");
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont); assocLbl.setFont(labelFont);
topPanel.add(assocLbl, gbc); topPanel.add(assocLbl, gbc);
gbc.gridx = 3; gbc.weightx = 0.5; gbc.gridx = 3; gbc.weightx = 0.5;
displayLuField = createDarkField("", 10); displayLuField = createField("", 10);
topPanel.add(displayLuField, gbc); topPanel.add(displayLuField, gbc);
// CodePage & TLS // CodePage & TLS
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0; gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
JLabel cpLbl = new JLabel("CodePage:"); JLabel cpLbl = new JLabel("CodePage:");
cpLbl.setForeground(DARK_FG); cpLbl.setFont(labelFont); cpLbl.setFont(labelFont);
topPanel.add(cpLbl, gbc); topPanel.add(cpLbl, gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridx = 1; gbc.weightx = 1.0;
codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"}); codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"});
codePageCombo.setBackground(DARK_FIELD_BG); ThemeManager.styleComboBox(codePageCombo);
codePageCombo.setForeground(DARK_FG);
topPanel.add(codePageCombo, gbc); topPanel.add(codePageCombo, gbc);
gbc.gridx = 2; gbc.gridwidth = 2; gbc.gridx = 2; gbc.gridwidth = 2;
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
tlsPanel.setOpaque(false); tlsPanel.setOpaque(false);
tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls()); tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls());
tlsCheck.setOpaque(false); tlsCheck.setForeground(DARK_FG); tlsCheck.setFocusPainted(false); ThemeManager.styleCheckBox(tlsCheck);
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert()); verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
verifyCertCheck.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false); ThemeManager.styleCheckBox(verifyCertCheck);
tlsPanel.add(tlsCheck); tlsPanel.add(tlsCheck);
tlsPanel.add(verifyCertCheck); tlsPanel.add(verifyCertCheck);
topPanel.add(tlsPanel, gbc); topPanel.add(tlsPanel, gbc);
@@ -144,23 +136,22 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Destination Type & Target // Destination Type & Target
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1; gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
JLabel destLbl = new JLabel("Destination:"); JLabel destLbl = new JLabel("Destination:");
destLbl.setForeground(DARK_FG); destLbl.setFont(labelFont); destLbl.setFont(labelFont);
topPanel.add(destLbl, gbc); topPanel.add(destLbl, gbc);
gbc.gridx = 1; gbc.gridx = 1;
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values()); destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY); destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
destinationCombo.setBackground(DARK_FIELD_BG); ThemeManager.styleComboBox(destinationCombo);
destinationCombo.setForeground(DARK_FG);
topPanel.add(destinationCombo, gbc); topPanel.add(destinationCombo, gbc);
gbc.gridx = 2; gbc.gridx = 2;
JLabel tgtLbl = new JLabel("Target Path:"); JLabel tgtLbl = new JLabel("Target Path:");
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont); tgtLbl.setFont(labelFont);
topPanel.add(tgtLbl, gbc); topPanel.add(tgtLbl, gbc);
gbc.gridx = 3; gbc.gridx = 3;
targetField = createDarkField("printer_output.txt", 12); targetField = createField("printer_output.txt", 12);
topPanel.add(targetField, gbc); topPanel.add(targetField, gbc);
// Connect / Disconnect Buttons // Connect / Disconnect Buttons
@@ -169,13 +160,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
connBtnPan.setOpaque(false); connBtnPan.setOpaque(false);
connectBtn = new JButton("Start Printer Session"); connectBtn = new JButton("Start Printer Session");
connectBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
connectBtn.setForeground(Color.WHITE);
connectBtn.addActionListener(e -> startPrinterSession()); connectBtn.addActionListener(e -> startPrinterSession());
disconnectBtn = new JButton("Stop Session"); disconnectBtn = new JButton("Stop Session");
disconnectBtn.setBackground(new Color(120, 50, 50)); ThemeManager.styleButton(disconnectBtn, ThemeManager.ButtonVariant.DANGER);
disconnectBtn.setForeground(Color.WHITE);
disconnectBtn.setEnabled(false); disconnectBtn.setEnabled(false);
disconnectBtn.addActionListener(e -> stopPrinterSession()); disconnectBtn.addActionListener(e -> stopPrinterSession());
@@ -188,9 +177,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Center: Spool Area & Status // Center: Spool Area & Status
JPanel centerPanel = new JPanel(new BorderLayout(6, 6)); JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
centerPanel.setOpaque(false); centerPanel.setOpaque(false);
centerPanel.setBorder(BorderFactory.createTitledBorder( centerPanel.setBorder(ThemeManager.createTitledBorder("Printer Spool & Status"));
new LineBorder(new Color(65, 65, 65)), "Printer Spool & Status"));
((TitledBorder) centerPanel.getBorder()).setTitleColor(DARK_FG);
// Status Header // Status Header
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0)); JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
@@ -198,19 +185,16 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4)); statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
statusLabel = new JLabel("Status: Disconnected"); statusLabel = new JLabel("Status: Disconnected");
statusLabel.setForeground(new Color(180, 180, 180)); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont); statusLabel.setFont(labelFont);
sessionTypeLabel = new JLabel("Session: -"); sessionTypeLabel = new JLabel("Session: -");
sessionTypeLabel.setForeground(DARK_FG);
sessionTypeLabel.setFont(labelFont); sessionTypeLabel.setFont(labelFont);
pagesLabel = new JLabel("Pages: 0"); pagesLabel = new JLabel("Pages: 0");
pagesLabel.setForeground(DARK_FG);
pagesLabel.setFont(labelFont); pagesLabel.setFont(labelFont);
bytesLabel = new JLabel("Bytes: 0"); bytesLabel = new JLabel("Bytes: 0");
bytesLabel.setForeground(DARK_FG);
bytesLabel.setFont(labelFont); bytesLabel.setFont(labelFont);
statusHeader.add(statusLabel); statusHeader.add(statusLabel);
@@ -221,14 +205,12 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area // Spool text area
spoolArea = new JTextArea(); spoolArea = new JTextArea();
spoolArea.setBackground(new Color(25, 25, 25));
spoolArea.setForeground(new Color(100, 255, 100));
spoolArea.setCaretColor(Color.WHITE);
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
spoolArea.setEditable(false); spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea);
JScrollPane spoolScroll = new JScrollPane(spoolArea); JScrollPane spoolScroll = new JScrollPane(spoolArea);
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60))); ThemeManager.styleScrollPane(spoolScroll);
centerPanel.add(spoolScroll, BorderLayout.CENTER); centerPanel.add(spoolScroll, BorderLayout.CENTER);
mainPanel.add(centerPanel, BorderLayout.CENTER); mainPanel.add(centerPanel, BorderLayout.CENTER);
@@ -238,8 +220,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
JButton clearSpoolBtn = new JButton("Clear Spool"); JButton clearSpoolBtn = new JButton("Clear Spool");
clearSpoolBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(clearSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
clearSpoolBtn.setForeground(DARK_FG);
clearSpoolBtn.addActionListener(e -> { clearSpoolBtn.addActionListener(e -> {
spoolArea.setText(""); spoolArea.setText("");
if (printerSession != null && printerSession.getPD() != null) { if (printerSession != null && printerSession.getPD() != null) {
@@ -250,18 +231,15 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
}); });
JButton saveSpoolBtn = new JButton("Save Spool As..."); JButton saveSpoolBtn = new JButton("Save Spool As...");
saveSpoolBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(saveSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
saveSpoolBtn.setForeground(DARK_FG);
saveSpoolBtn.addActionListener(e -> saveSpool()); saveSpoolBtn.addActionListener(e -> saveSpool());
JButton printSpoolBtn = new JButton("Print Spool..."); JButton printSpoolBtn = new JButton("Print Spool...");
printSpoolBtn.setBackground(new Color(50, 100, 160)); ThemeManager.styleButton(printSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
printSpoolBtn.setForeground(Color.WHITE);
printSpoolBtn.addActionListener(e -> printSpool()); printSpoolBtn.addActionListener(e -> printSpool());
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(DARK_FG);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
bottomPanel.add(clearSpoolBtn); bottomPanel.add(clearSpoolBtn);
@@ -273,17 +251,13 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
} }
private JTextField createDarkField(String text, int cols) { private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols); JTextField tf = new JTextField(text, cols);
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
tf.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleTextField(tf);
new LineBorder(new Color(65, 65, 65)),
new EmptyBorder(2, 4, 2, 4)));
return tf; return tf;
} }
@@ -34,9 +34,7 @@ public class ScriptDialog extends JDialog {
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
mainPanel.setBackground(new Color(35, 35, 35));
Color fg = new Color(220, 220, 220);
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
// Header // Header
@@ -44,31 +42,24 @@ public class ScriptDialog extends JDialog {
topPanel.setOpaque(false); topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>" JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>"
+ "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>"); + "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>");
descLabel.setForeground(new Color(180, 180, 180));
descLabel.setFont(labelFont); descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER); topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH); mainPanel.add(topPanel, BorderLayout.NORTH);
// Script area // Script area
scriptArea = new JTextArea(); scriptArea = new JTextArea();
scriptArea.setBackground(new Color(25, 25, 25));
scriptArea.setForeground(new Color(50, 205, 50));
scriptArea.setCaretColor(Color.WHITE);
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
scriptArea.setLineWrap(true); scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false); scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea);
JScrollPane scrollPane = new JScrollPane(scriptArea); JScrollPane scrollPane = new JScrollPane(scriptArea);
scrollPane.setBorder(BorderFactory.createCompoundBorder( ThemeManager.styleScrollPane(scrollPane);
new LineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 2, 2, 2)));
// Mnemonic helper buttons // Mnemonic helper buttons
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4)); JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
tokenPanel.setOpaque(false); tokenPanel.setOpaque(false);
tokenPanel.setBorder(BorderFactory.createTitledBorder( tokenPanel.setBorder(ThemeManager.createTitledBorder("Insert Keystroke Token"));
new LineBorder(new Color(60, 60, 60)), "Insert Keystroke Token"));
((TitledBorder) tokenPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
String[] tokens = { String[] tokens = {
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]", "[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
@@ -84,8 +75,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) { for (String token : tokens) {
JButton btn = new JButton(token); JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
btn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setForeground(fg);
btn.setFocusable(false); btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4)); btn.setMargin(new Insets(2, 4, 2, 4));
btn.addActionListener(e -> insertToken(token)); btn.addActionListener(e -> insertToken(token));
@@ -103,7 +93,7 @@ public class ScriptDialog extends JDialog {
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
statusLabel = new JLabel("Ready"); statusLabel = new JLabel("Ready");
statusLabel.setForeground(new Color(160, 160, 160)); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
@@ -111,26 +101,22 @@ public class ScriptDialog extends JDialog {
buttonPanel.setOpaque(false); buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load Script..."); JButton loadBtn = new JButton("Load Script...");
loadBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setForeground(fg);
loadBtn.setFont(labelFont); loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadScript()); loadBtn.addActionListener(e -> loadScript());
JButton saveBtn = new JButton("Save Script..."); JButton saveBtn = new JButton("Save Script...");
saveBtn.setBackground(new Color(50, 50, 50)); ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setForeground(fg);
saveBtn.setFont(labelFont); saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveScript()); saveBtn.addActionListener(e -> saveScript());
JButton runBtn = new JButton("Execute"); JButton runBtn = new JButton("Execute");
runBtn.setBackground(new Color(50, 120, 50)); ThemeManager.styleButton(runBtn, ThemeManager.ButtonVariant.PRIMARY);
runBtn.setForeground(Color.WHITE);
runBtn.setFont(labelFont); runBtn.setFont(labelFont);
runBtn.addActionListener(e -> executeScript()); runBtn.addActionListener(e -> executeScript());
JButton closeBtn = new JButton("Close"); JButton closeBtn = new JButton("Close");
closeBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setForeground(fg);
closeBtn.setFont(labelFont); closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose()); closeBtn.addActionListener(e -> dispose());
@@ -144,6 +130,7 @@ public class ScriptDialog extends JDialog {
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
} }
private void insertToken(String token) { private void insertToken(String token) {
@@ -19,16 +19,8 @@ public class SettingsDialog extends JDialog {
private final J3270App parentApp; private final J3270App parentApp;
// Dark theme colors
private static final Color DARK_BG = new Color(43, 43, 43);
private static final Color DARK_BG_LIGHTER = new Color(55, 55, 55);
private static final Color DARK_FG = new Color(224, 224, 224);
private static final Color DARK_BORDER = new Color(70, 70, 70);
private static final Color DARK_SELECTION = new Color(75, 110, 175);
private static final Color DARK_BUTTON_BG = new Color(60, 63, 65);
private static final Color DARK_FIELD_BG = new Color(50, 50, 50);
// Appearance tab // Appearance tab
private JComboBox<UITheme> uiThemeBox;
private JComboBox<String> fontBox; private JComboBox<String> fontBox;
private JSpinner fontSizeSpinner; private JSpinner fontSizeSpinner;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox; private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
@@ -51,22 +43,30 @@ public class SettingsDialog extends JDialog {
this.parentApp = parent; this.parentApp = parent;
initComponents(); initComponents();
setSize(550, 450); setSize(560, 480);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
private void initComponents() { private void initComponents() {
JTabbedPane tabbedPane = new JTabbedPane(); JTabbedPane tabbedPane = new JTabbedPane();
ThemeManager.styleTabbedPane(tabbedPane);
tabbedPane.addTab("Appearance", createAppearancePanel()); tabbedPane.addTab("Appearance", createAppearancePanel());
tabbedPane.addTab("Behavior", createBehaviorPanel()); tabbedPane.addTab("Behavior", createBehaviorPanel());
tabbedPane.addTab("Advanced", createAdvancedPanel()); tabbedPane.addTab("Advanced", createAdvancedPanel());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
JButton btnExport = new JButton("Export Config..."); JButton btnExport = new JButton("Export Config...");
JButton btnOk = new JButton("OK"); ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
JButton btnApply = new JButton("Apply"); JButton btnApply = new JButton("Apply");
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
JButton btnCancel = new JButton("Cancel"); JButton btnCancel = new JButton("Cancel");
ThemeManager.styleButton(btnCancel, ThemeManager.ButtonVariant.CANCEL);
JButton btnOk = new JButton("OK");
ThemeManager.styleButton(btnOk, ThemeManager.ButtonVariant.PRIMARY);
btnExport.addActionListener((ActionEvent e) -> { btnExport.addActionListener((ActionEvent e) -> {
exportConfig(); exportConfig();
@@ -96,132 +96,11 @@ public class SettingsDialog extends JDialog {
getContentPane().add(tabbedPane, BorderLayout.CENTER); getContentPane().add(tabbedPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH); getContentPane().add(buttonPanel, BorderLayout.SOUTH);
// Apply dark theme to all components for cross-platform readability ThemeManager.applyThemeToWindow(this);
applyDarkTheme(getContentPane());
applyDarkTheme(tabbedPane);
applyDarkTheme(buttonPanel);
getContentPane().setBackground(DARK_BG);
} }
// ========== Dark Theme Utility ==========
/**
* Recursively apply dark theme to a component and all its children.
* Ensures the Settings dialog is readable on Windows, Linux, and macOS.
*/
private void applyDarkTheme(Component comp) { private void applyDarkTheme(Component comp) {
if (comp instanceof JTabbedPane) { ThemeManager.applyTheme(comp);
JTabbedPane tp = (JTabbedPane) comp;
tp.setBackground(DARK_BG);
tp.setForeground(DARK_FG);
for (int i = 0; i < tp.getTabCount(); i++) {
applyDarkTheme(tp.getComponentAt(i));
}
return;
}
if (comp instanceof JTable) {
JTable table = (JTable) comp;
table.setBackground(DARK_FIELD_BG);
table.setForeground(DARK_FG);
table.setSelectionBackground(DARK_SELECTION);
table.setSelectionForeground(Color.WHITE);
table.setGridColor(DARK_BORDER);
JTableHeader header = table.getTableHeader();
if (header != null) {
header.setBackground(DARK_BG_LIGHTER);
header.setForeground(DARK_FG);
DefaultTableCellRenderer headerRenderer = new DefaultTableCellRenderer();
headerRenderer.setBackground(DARK_BG_LIGHTER);
headerRenderer.setForeground(DARK_FG);
header.setDefaultRenderer(headerRenderer);
}
return;
}
if (comp instanceof JScrollPane) {
JScrollPane sp = (JScrollPane) comp;
sp.setBackground(DARK_BG);
sp.getViewport().setBackground(DARK_FIELD_BG);
applyDarkTheme(sp.getViewport().getView());
return;
}
if (comp instanceof JButton) {
JButton btn = (JButton) comp;
btn.setBackground(DARK_BUTTON_BG);
btn.setForeground(DARK_FG);
btn.setFocusPainted(false);
btn.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(DARK_BORDER, 1),
BorderFactory.createEmptyBorder(3, 10, 3, 10)));
btn.setOpaque(true);
return;
}
if (comp instanceof JComboBox) {
JComboBox<?> cb = (JComboBox<?>) comp;
cb.setBackground(DARK_FIELD_BG);
cb.setForeground(DARK_FG);
return;
}
if (comp instanceof JSpinner) {
JSpinner sp = (JSpinner) comp;
sp.setBackground(DARK_FIELD_BG);
sp.setForeground(DARK_FG);
JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
}
return;
}
if (comp instanceof JTextField) {
JTextField tf = (JTextField) comp;
tf.setBackground(DARK_FIELD_BG);
tf.setForeground(DARK_FG);
tf.setCaretColor(DARK_FG);
return;
}
if (comp instanceof JCheckBox) {
JCheckBox cb = (JCheckBox) comp;
cb.setBackground(DARK_BG);
cb.setForeground(DARK_FG);
return;
}
if (comp instanceof JLabel) {
comp.setForeground(DARK_FG);
return;
}
// Generic panel / container
// Skip color swatch panels — their background IS the color
if (comp instanceof JPanel && "colorSwatch".equals(comp.getName())) {
return;
}
comp.setBackground(DARK_BG);
comp.setForeground(DARK_FG);
if (comp instanceof JPanel) {
JPanel panel = (JPanel) comp;
// Style titled borders
if (panel.getBorder() instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) panel.getBorder();
tb.setTitleColor(DARK_FG);
}
}
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyDarkTheme(child);
}
}
} }
// ========== Tab Panels ========== // ========== Tab Panels ==========
@@ -232,9 +111,22 @@ public class SettingsDialog extends JDialog {
gbc.insets = new Insets(10, 10, 10, 10); gbc.insets = new Insets(10, 10, 10, 10);
gbc.anchor = GridBagConstraints.WEST; gbc.anchor = GridBagConstraints.WEST;
// Font family // UI Theme
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = 0;
JLabel themeLabel = new JLabel("Java UI Theme:");
panel.add(themeLabel, gbc);
uiThemeBox = new JComboBox<>(UITheme.values());
uiThemeBox.setSelectedItem(Settings.getJavaUiTheme());
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(uiThemeBox, gbc);
// Font family
gbc.gridx = 0;
gbc.gridy = 1;
gbc.fill = GridBagConstraints.NONE;
JLabel fontLabel = new JLabel("Terminal Font:"); JLabel fontLabel = new JLabel("Terminal Font:");
panel.add(fontLabel, gbc); panel.add(fontLabel, gbc);
@@ -254,7 +146,7 @@ public class SettingsDialog extends JDialog {
// Font size // Font size
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = 2;
gbc.fill = GridBagConstraints.NONE; gbc.fill = GridBagConstraints.NONE;
JLabel sizeLabel = new JLabel("Font Size:"); JLabel sizeLabel = new JLabel("Font Size:");
panel.add(sizeLabel, gbc); panel.add(sizeLabel, gbc);
@@ -267,7 +159,7 @@ public class SettingsDialog extends JDialog {
// Graphics Mode // Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 2; gbc.gridy = 3;
gbc.fill = GridBagConstraints.NONE; gbc.fill = GridBagConstraints.NONE;
JLabel graphicsLabel = new JLabel("Graphics Mode:"); JLabel graphicsLabel = new JLabel("Graphics Mode:");
panel.add(graphicsLabel, gbc); panel.add(graphicsLabel, gbc);
@@ -279,7 +171,7 @@ public class SettingsDialog extends JDialog {
panel.add(graphicsModeBox, gbc); panel.add(graphicsModeBox, gbc);
// Fill remaining space // Fill remaining space
gbc.gridy = 3; gbc.gridy = 4;
gbc.weighty = 1.0; gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc); panel.add(Box.createGlue(), gbc);
@@ -429,6 +321,7 @@ public class SettingsDialog extends JDialog {
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnResetColors = new JButton("Reset to Defaults"); JButton btnResetColors = new JButton("Reset to Defaults");
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
btnResetColors.addActionListener(e -> { btnResetColors.addActionListener(e -> {
for (int i=0; i<16; i++) { for (int i=0; i<16; i++) {
tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i]; tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i];
@@ -436,8 +329,6 @@ public class SettingsDialog extends JDialog {
for (int i=0; i<monoKeys.length; i++) { for (int i=0; i<monoKeys.length; i++) {
tempMonoColors.put(monoKeys[i], monoDefs[i]); tempMonoColors.put(monoKeys[i], monoDefs[i]);
} }
// Repaint container implicitly handled if we trigger a UI update,
// but for simplicity user can just close/reopen or have it refresh on save
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save."); JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
}); });
resetPanel.add(btnResetColors); resetPanel.add(btnResetColors);
@@ -498,6 +389,7 @@ public class SettingsDialog extends JDialog {
} }
JTable table = new JTable(keymapModel); JTable table = new JTable(keymapModel);
ThemeManager.styleTable(table);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setFillsViewportHeight(true); table.setFillsViewportHeight(true);
@@ -510,9 +402,8 @@ public class SettingsDialog extends JDialog {
captureDialog.setSize(320, 100); captureDialog.setSize(320, 100);
captureDialog.setLocationRelativeTo(this); captureDialog.setLocationRelativeTo(this);
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER); JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
lbl.setForeground(DARK_FG);
captureDialog.getContentPane().setBackground(DARK_BG);
captureDialog.add(lbl); captureDialog.add(lbl);
ThemeManager.applyThemeToWindow(captureDialog);
captureDialog.addKeyListener(new KeyAdapter() { captureDialog.addKeyListener(new KeyAdapter() {
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
@@ -532,6 +423,7 @@ public class SettingsDialog extends JDialog {
// Replace Binding — sets the action to exactly one new key // Replace Binding — sets the action to exactly one new key
JButton btnReplace = new JButton("Replace Binding"); JButton btnReplace = new JButton("Replace Binding");
ThemeManager.styleButton(btnReplace, ThemeManager.ButtonVariant.DEFAULT);
btnReplace.addActionListener(e -> { btnReplace.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -545,6 +437,7 @@ public class SettingsDialog extends JDialog {
// Add Binding — appends an additional key to the existing binding(s) // Add Binding — appends an additional key to the existing binding(s)
JButton btnAdd = new JButton("Add Binding"); JButton btnAdd = new JButton("Add Binding");
ThemeManager.styleButton(btnAdd, ThemeManager.ButtonVariant.DEFAULT);
btnAdd.addActionListener(e -> { btnAdd.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -565,6 +458,7 @@ public class SettingsDialog extends JDialog {
// Remove Last — removes the last comma-separated binding entry // Remove Last — removes the last comma-separated binding entry
JButton btnRemoveLast = new JButton("Remove Last"); JButton btnRemoveLast = new JButton("Remove Last");
ThemeManager.styleButton(btnRemoveLast, ThemeManager.ButtonVariant.DEFAULT);
btnRemoveLast.addActionListener(e -> { btnRemoveLast.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -585,6 +479,7 @@ public class SettingsDialog extends JDialog {
// Unbind All — clears all bindings for the action // Unbind All — clears all bindings for the action
JButton btnUnbind = new JButton("Unbind All"); JButton btnUnbind = new JButton("Unbind All");
ThemeManager.styleButton(btnUnbind, ThemeManager.ButtonVariant.DEFAULT);
btnUnbind.addActionListener(e -> { btnUnbind.addActionListener(e -> {
int row = table.getSelectedRow(); int row = table.getSelectedRow();
if (row < 0) return; if (row < 0) return;
@@ -596,6 +491,7 @@ public class SettingsDialog extends JDialog {
// Reset Keymaps — restore all defaults // Reset Keymaps — restore all defaults
JButton btnReset = new JButton("Reset All"); JButton btnReset = new JButton("Reset All");
ThemeManager.styleButton(btnReset, ThemeManager.ButtonVariant.DEFAULT);
btnReset.addActionListener(e -> { btnReset.addActionListener(e -> {
for (int row = 0; row < keymapModel.getRowCount(); row++) { for (int row = 0; row < keymapModel.getRowCount(); row++) {
String action = (String) keymapModel.getValueAt(row, 0); String action = (String) keymapModel.getValueAt(row, 0);
@@ -628,6 +524,12 @@ public class SettingsDialog extends JDialog {
private boolean applySettings() { private boolean applySettings() {
try { try {
// Apply Appearance // Apply Appearance
UITheme selectedTheme = (UITheme) uiThemeBox.getSelectedItem();
if (selectedTheme != null) {
Settings.setJavaUiTheme(selectedTheme);
ThemeManager.setTheme(selectedTheme);
}
String fontFam = (String) fontBox.getSelectedItem(); String fontFam = (String) fontBox.getSelectedItem();
if (fontFam != null) { if (fontFam != null) {
Settings.setFontFamily(fontFam); Settings.setFontFamily(fontFam);
@@ -668,6 +570,7 @@ public class SettingsDialog extends JDialog {
} }
parentApp.getTerminalPanel().reloadSettings(); parentApp.getTerminalPanel().reloadSettings();
ThemeManager.applyThemeToWindow(this);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
@@ -26,29 +26,22 @@ public class StatusBar extends JPanel {
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
// OIA colors
private static final Color OIA_BG = new Color(20, 20, 20);
private static final Color OIA_FG = new Color(50, 205, 50);
private static final Color OIA_DIM = new Color(80, 80, 80);
private static final Color OIA_ALERT = new Color(255, 80, 80);
private static final Color OIA_WARN = new Color(255, 200, 80);
public StatusBar() { public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(OIA_BG); setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40))); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
setPreferredSize(new Dimension(800, 22)); setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, OIA_FG); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
luName = createLabel("", oiaFont, OIA_FG); luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
lockStatus = createLabel("", oiaFont, OIA_ALERT); lockStatus = createLabel("", oiaFont, ThemeManager.getOiaFgAlert());
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM); fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
codePageInfo = createLabel("", oiaFont, OIA_DIM); codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
modelInfo = createLabel("", oiaFont, OIA_DIM); modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG); cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
add(Box.createHorizontalStrut(6)); add(Box.createHorizontalStrut(6));
add(connectionStatus); add(connectionStatus);
@@ -82,10 +75,17 @@ public class StatusBar extends JPanel {
updateStatus(); updateStatus();
} }
public void applyTheme(UITheme theme) {
setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
updateStatus();
}
public void updateStatus() { public void updateStatus() {
UITheme theme = ThemeManager.getTheme();
if (client == null) { if (client == null) {
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
tlsStatus.setText(""); tlsStatus.setText("");
luName.setText(""); luName.setText("");
lockStatus.setText(""); lockStatus.setText("");
@@ -101,38 +101,38 @@ public class StatusBar extends JPanel {
switch (state) { switch (state) {
case NOT_CONNECTED: case NOT_CONNECTED:
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break; break;
case TCP_PENDING: case TCP_PENDING:
case TELNET_PENDING: case TELNET_PENDING:
connectionStatus.setText("Connecting..."); connectionStatus.setText("Connecting...");
connectionStatus.setForeground(OIA_ALERT); connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
break; break;
case CONNECTED_3270: case CONNECTED_3270:
connectionStatus.setText("TN3270"); connectionStatus.setText("TN3270");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_TN3270E: case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E"); connectionStatus.setText("TN3270E");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_SSCP: case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU"); connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_NVT: case CONNECTED_NVT:
case CONNECTED_NVT_CHAR: case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT: case CONNECTED_E_NVT:
connectionStatus.setText("NVT"); connectionStatus.setText("NVT");
connectionStatus.setForeground(OIA_FG); connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
break; break;
case CONNECTED_UNBOUND: case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound"); connectionStatus.setText("Unbound");
connectionStatus.setForeground(OIA_WARN); connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
break; break;
default: default:
connectionStatus.setText(state.name()); connectionStatus.setText(state.name());
connectionStatus.setForeground(OIA_DIM); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break; break;
} }
@@ -144,11 +144,11 @@ public class StatusBar extends JPanel {
String protocol = session != null ? session.getProtocol() : "TLS"; String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) { if (verified) {
tlsStatus.setText("🔒 TLS"); tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(OIA_FG); tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else { } else {
tlsStatus.setText("🔓 TLS (Unverified)"); tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(new Color(255, 180, 80)); tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
} }
} else { } else {
@@ -164,6 +164,7 @@ public class StatusBar extends JPanel {
lu = "LU:" + client.getConfig().getLuName(); lu = "LU:" + client.getConfig().getLuName();
} }
luName.setText(lu); luName.setText(lu);
luName.setForeground(ThemeManager.getOiaFgNormal(theme));
// Lock / Inhibit status // Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited(); int inhibit = client.getOIA().getInputInhibited();
@@ -191,10 +192,10 @@ public class StatusBar extends JPanel {
lockStatus.setText("X LOCKED"); lockStatus.setText("X LOCKED");
break; break;
} }
lockStatus.setForeground(OIA_ALERT); lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
} else if (client.getInputProcessor().isInsertMode()) { } else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT"); lockStatus.setText("INSERT");
lockStatus.setForeground(OIA_FG); lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
} else { } else {
lockStatus.setText(""); lockStatus.setText("");
} }
@@ -203,10 +204,10 @@ public class StatusBar extends JPanel {
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) { if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM"); fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(OIA_WARN); fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else { } else {
fieldTypeStatus.setText("ALPHA"); fieldTypeStatus.setText("ALPHA");
fieldTypeStatus.setForeground(OIA_DIM); fieldTypeStatus.setForeground(ThemeManager.getOiaFgDim(theme));
} }
} else { } else {
fieldTypeStatus.setText(""); fieldTypeStatus.setText("");
@@ -215,6 +216,7 @@ public class StatusBar extends JPanel {
// Active Code Page // Active Code Page
String cp = client.getCodePage(); String cp = client.getCodePage();
codePageInfo.setText(cp != null ? "CP" + cp : ""); codePageInfo.setText(cp != null ? "CP" + cp : "");
codePageInfo.setForeground(ThemeManager.getOiaFgDim(theme));
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp); codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
// Model & Dimensions info // Model & Dimensions info
@@ -222,11 +224,13 @@ public class StatusBar extends JPanel {
int rows = sb.getDisplayRows(); int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols(); int cols = sb.getDisplayCols();
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]"); modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
// Cursor position and buffer address // Cursor position and buffer address
int curAddr = sb.getCursorAddress(); int curAddr = sb.getCursorAddress();
int row = sb.getCursorRow() + 1; int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1; int col = sb.getCursorCol() + 1;
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr)); cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
} }
} }
@@ -123,7 +123,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255); public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
// Default Background // Default Background
public static final Color DEFAULT_BG_COLOR = new Color(10, 10, 10); public static final Color DEFAULT_BG_COLOR = Color.BLACK;
public TerminalPanel() { public TerminalPanel() {
setupColors(); setupColors();
@@ -221,8 +221,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int ox = getRenderOffsetX(); int ox = getRenderOffsetX();
int oy = getRenderOffsetY(); int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
int gridW = sb.getDisplayCols() * cellWidth; int gridW = (sb != null ? sb.getDisplayCols() : 80) * cellWidth;
int gridH = sb.getDisplayRows() * cellHeight; int gridH = (sb != null ? sb.getDisplayRows() : 24) * cellHeight;
int gWidth = client.getGraphicsPlane().getCanvasWidth(); int gWidth = client.getGraphicsPlane().getCanvasWidth();
int gHeight = client.getGraphicsPlane().getCanvasHeight(); int gHeight = client.getGraphicsPlane().getCanvasHeight();
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox); int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
@@ -253,6 +253,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (isGraphic) { if (isGraphic) {
clearSelection(); clearSelection();
sb.setCursorAddress(clickAddr);
int gridW = displayCols * cellWidth; int gridW = displayCols * cellWidth;
int gridH = displayRows * cellHeight; int gridH = displayRows * cellHeight;
int gWidth = client.getGraphicsPlane().getCanvasWidth(); int gWidth = client.getGraphicsPlane().getCanvasWidth();
@@ -433,6 +434,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
public void pasteClipboard() { public void pasteClipboard() {
if (client == null || !client.getConnectionState().isFullSession()) return; if (client == null || !client.getConnectionState().isFullSession()) return;
if (client.getConnectionState().isNvt()) {
try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor);
if (text != null && !text.isEmpty()) {
client.sendNVTString(text);
}
} catch (Exception ignored) {}
return;
}
try { try {
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard() String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(DataFlavor.stringFlavor); .getData(DataFlavor.stringFlavor);
@@ -480,24 +491,29 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void showContextMenu(MouseEvent e) { private void showContextMenu(MouseEvent e) {
JPopupMenu popup = new JPopupMenu(); JPopupMenu popup = new JPopupMenu();
ThemeManager.stylePopupMenu(popup);
JMenuItem copyItem = new JMenuItem("Copy"); JMenuItem copyItem = new JMenuItem("Copy");
ThemeManager.styleMenuItem(copyItem);
copyItem.setEnabled(hasSelection()); copyItem.setEnabled(hasSelection());
copyItem.addActionListener(ev -> copySelection()); copyItem.addActionListener(ev -> copySelection());
popup.add(copyItem); popup.add(copyItem);
JMenuItem pasteItem = new JMenuItem("Paste"); JMenuItem pasteItem = new JMenuItem("Paste");
ThemeManager.styleMenuItem(pasteItem);
pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession()); pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession());
pasteItem.addActionListener(ev -> pasteClipboard()); pasteItem.addActionListener(ev -> pasteClipboard());
popup.add(pasteItem); popup.add(pasteItem);
JMenuItem selectAllItem = new JMenuItem("Select All"); JMenuItem selectAllItem = new JMenuItem("Select All");
ThemeManager.styleMenuItem(selectAllItem);
selectAllItem.addActionListener(ev -> selectAll()); selectAllItem.addActionListener(ev -> selectAll());
popup.add(selectAllItem); popup.add(selectAllItem);
popup.addSeparator(); popup.addSeparator();
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode); JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
ThemeManager.styleMenuItem(blockModeItem);
blockModeItem.addActionListener(ev -> { blockModeItem.addActionListener(ev -> {
blockSelectMode = blockModeItem.isSelected(); blockSelectMode = blockModeItem.isSelected();
haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode); haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
@@ -692,19 +708,73 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
@Override @Override
protected void processKeyEvent(KeyEvent e) { protected void processKeyEvent(KeyEvent e) {
if (e.getID() == KeyEvent.KEY_TYPED) { if (client != null) {
char ch = e.getKeyChar(); ConnectionState state = client.getConnectionState();
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED if (state.isNvt()) {
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) { if (e.getID() == KeyEvent.KEY_TYPED) {
if (client != null) { char ch = e.getKeyChar();
ConnectionState state = client.getConnectionState(); if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) { && !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
try { try {
client.sendNVTChar(ch); client.sendNVTChar(ch);
} catch (Exception ignored) {} } catch (Exception ignored) {}
e.consume(); e.consume();
return; return;
} else if (state.isFullSession()) { } else if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
if (ch > 0 && ch < 0x20) {
try {
client.sendNVTChar(ch);
} catch (Exception ignored) {}
e.consume();
return;
}
}
} else if (e.getID() == KeyEvent.KEY_PRESSED) {
if (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); client.typeCharacter(ch);
refreshScreen(); refreshScreen();
e.consume(); e.consume();
@@ -730,7 +800,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleEnter() { private void handleEnter() {
if (client != null) { if (client != null) {
ConnectionState state = client.getConnectionState(); ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) { if (state.isNvt()) {
try { try {
client.sendNVTString("\r\n"); client.sendNVTString("\r\n");
} catch (Exception ignored) {} } catch (Exception ignored) {}
@@ -743,6 +813,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleReset() { private void handleReset() {
if (client != null) { if (client != null) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTChar('\u001B');
} catch (Exception ignored) {}
return;
}
clearSearchHighlight(); clearSearchHighlight();
clearSelection(); clearSelection();
client.reset(); client.reset();
@@ -752,6 +828,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleTab(boolean shift) { private void handleTab(boolean shift) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
if (shift) {
client.sendNVTString("\u001B[Z");
} else {
client.sendNVTChar('\t');
}
} catch (Exception ignored) {}
return;
}
if (shift) client.backTab(); if (shift) client.backTab();
else client.tab(); else client.tab();
refreshScreen(); refreshScreen();
@@ -760,6 +846,26 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleCursor(String dir) { private void handleCursor(String dir) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
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) { switch (dir) {
case "up": client.cursorUp(); break; case "up": client.cursorUp(); break;
case "down": client.cursorDown(); break; case "down": client.cursorDown(); break;
@@ -773,11 +879,39 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handlePF(int n) { private void handlePF(int n) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
if (client.getNvtProcessor() != null) {
client.sendNVTString(client.getNvtProcessor().getFunctionKeySequence(n));
} else {
client.sendNVTString(getNvtFunctionKeySequence(n));
}
} catch (Exception ignored) {}
return;
}
client.sendPF(n); client.sendPF(n);
refreshScreen(); refreshScreen();
} }
} }
private String getNvtFunctionKeySequence(int n) {
switch (n) {
case 1: return "\u001BOP";
case 2: return "\u001BOQ";
case 3: return "\u001BOR";
case 4: return "\u001BOS";
case 5: return "\u001B[15~";
case 6: return "\u001B[17~";
case 7: return "\u001B[18~";
case 8: return "\u001B[19~";
case 9: return "\u001B[20~";
case 10: return "\u001B[21~";
case 11: return "\u001B[23~";
case 12: return "\u001B[24~";
default: return "";
}
}
private void handlePA(int n) { private void handlePA(int n) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
client.sendPA(n); client.sendPA(n);
@@ -787,6 +921,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleEraseEOF() { private void handleEraseEOF() {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTString("\u001B[F");
} catch (Exception ignored) {}
return;
}
client.getInputProcessor().eraseEof(); client.getInputProcessor().eraseEof();
refreshScreen(); refreshScreen();
} }
@@ -794,6 +934,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleDelete() { private void handleDelete() {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTString("\u001B[3~");
} catch (Exception ignored) {}
return;
}
client.getInputProcessor().deleteChar(); client.getInputProcessor().deleteChar();
refreshScreen(); refreshScreen();
} }
@@ -802,7 +948,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleBackspace() { private void handleBackspace() {
if (client != null) { if (client != null) {
ConnectionState state = client.getConnectionState(); ConnectionState state = client.getConnectionState();
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) { if (state.isNvt()) {
try { try {
client.sendNVTChar('\b'); client.sendNVTChar('\b');
} catch (Exception ignored) {} } catch (Exception ignored) {}
@@ -823,6 +969,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private void handleClear() { private void handleClear() {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) {
try {
client.sendNVTChar('\u000C');
} catch (Exception ignored) {}
return;
}
client.sendClear(); client.sendClear();
refreshScreen(); refreshScreen();
} }
@@ -963,6 +1115,37 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (client != null) { if (client != null) {
setupGraphicsPlaneRenderer(); setupGraphicsPlaneRenderer();
updateCellSize(); 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");
}
});
});
} }
} }
@@ -995,11 +1178,17 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
double curX = x; double curX = x;
double curY = y; double curY = y;
if (dir == GocaConstants.CD_TB) {
curY += ch;
} else if (dir == GocaConstants.CD_RL) {
curX -= cw;
}
for (int i = 0; i < text.length(); i++) { for (int i = 0; i < text.length(); i++) {
String s = text.substring(i, i + 1); String s = text.substring(i, i + 1);
int charW = fm.stringWidth(s); int charW = fm.stringWidth(s);
int drawX = (int) Math.round(curX + Math.max(0, (cw - charW) / 2.0)); int drawX = (int) Math.round(curX + Math.max(0, (cw - charW) / 2.0));
int drawY = (int) Math.round(curY + ascent + Math.max(0, (ch - fm.getHeight()) / 2.0)); int drawY = (int) Math.round(curY);
g2.drawString(s, drawX, drawY); g2.drawString(s, drawX, drawY);
switch (dir) { switch (dir) {
@@ -0,0 +1,955 @@
package haus.nightmare.j3270.ui;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.plaf.basic.BasicButtonUI;
import javax.swing.plaf.basic.BasicMenuBarUI;
import javax.swing.plaf.basic.BasicMenuItemUI;
import javax.swing.plaf.basic.BasicMenuUI;
import javax.swing.plaf.basic.BasicPopupMenuUI;
import javax.swing.plaf.basic.BasicTabbedPaneUI;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Centralized theme manager for j3270 Java desktop UI.
* Provides high-contrast, platform-independent Light and Dark theme palettes,
* custom Swing UI delegates, and recursive styling helpers for all dialogs and menus.
*/
public final class ThemeManager {
private static UITheme currentTheme = UITheme.DARK;
private static final List<Consumer<UITheme>> themeChangeListeners = new ArrayList<>();
private ThemeManager() {}
public static UITheme getTheme() {
return currentTheme;
}
public static boolean isDark() {
return currentTheme == UITheme.DARK;
}
public static boolean isLight() {
return currentTheme == UITheme.LIGHT;
}
public static void setTheme(UITheme theme) {
if (theme == null) theme = UITheme.DARK;
if (currentTheme != theme) {
currentTheme = theme;
applyUIManagerDefaults(theme);
for (Consumer<UITheme> listener : new ArrayList<>(themeChangeListeners)) {
try {
listener.accept(theme);
} catch (Exception ignored) {}
}
}
}
public static void addThemeChangeListener(Consumer<UITheme> listener) {
if (listener != null && !themeChangeListeners.contains(listener)) {
themeChangeListeners.add(listener);
}
}
public static void removeThemeChangeListener(Consumer<UITheme> listener) {
themeChangeListeners.remove(listener);
}
// =========================================================================
// Color Palette Tokens
// =========================================================================
public static Color getBgMain() { return getBgMain(currentTheme); }
public static Color getBgMain(UITheme t) {
return t == UITheme.DARK ? new Color(28, 28, 28) : new Color(242, 242, 244);
}
public static Color getBgPanel() { return getBgPanel(currentTheme); }
public static Color getBgPanel(UITheme t) {
return t == UITheme.DARK ? new Color(36, 36, 36) : new Color(250, 250, 252);
}
public static Color getBgComponent() { return getBgComponent(currentTheme); }
public static Color getBgComponent(UITheme t) {
return t == UITheme.DARK ? new Color(46, 46, 46) : Color.WHITE;
}
public static Color getBgComponentLighter() { return getBgComponentLighter(currentTheme); }
public static Color getBgComponentLighter(UITheme t) {
return t == UITheme.DARK ? new Color(56, 56, 56) : new Color(236, 236, 238);
}
public static Color getFgMain() { return getFgMain(currentTheme); }
public static Color getFgMain(UITheme t) {
return t == UITheme.DARK ? new Color(238, 238, 238) : new Color(24, 24, 24);
}
public static Color getFgMuted() { return getFgMuted(currentTheme); }
public static Color getFgMuted(UITheme t) {
return t == UITheme.DARK ? new Color(175, 175, 175) : new Color(90, 90, 90);
}
public static Color getFgDisabled() { return getFgDisabled(currentTheme); }
public static Color getFgDisabled(UITheme t) {
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(160, 160, 160);
}
public static Color getBorder() { return getBorder(currentTheme); }
public static Color getBorder(UITheme t) {
return t == UITheme.DARK ? new Color(65, 65, 65) : new Color(205, 205, 205);
}
public static Color getBorderColor() { return getBorder(currentTheme); }
public static Color getBorderColor(UITheme t) { return getBorder(t); }
public static Color getBorderFocused() { return getBorderFocused(currentTheme); }
public static Color getBorderFocused(UITheme t) {
return t == UITheme.DARK ? new Color(80, 140, 230) : new Color(50, 115, 220);
}
public static Color getSelectionBg() { return getSelectionBg(currentTheme); }
public static Color getSelectionBg(UITheme t) {
return t == UITheme.DARK ? new Color(55, 105, 180) : new Color(60, 125, 215);
}
public static Color getSelectionFg() { return getSelectionFg(currentTheme); }
public static Color getSelectionFg(UITheme t) {
return Color.WHITE;
}
public static Color getMenuBarBg() { return getMenuBarBg(currentTheme); }
public static Color getMenuBarBg(UITheme t) {
return t == UITheme.DARK ? new Color(26, 26, 26) : new Color(240, 240, 242);
}
public static Color getMenuBarFg() { return getMenuBarFg(currentTheme); }
public static Color getMenuBarFg(UITheme t) {
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(25, 25, 25);
}
public static Color getMenuPopupBg() { return getMenuPopupBg(currentTheme); }
public static Color getMenuPopupBg(UITheme t) {
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
}
public static Color getMenuItemFg() { return getMenuItemFg(currentTheme); }
public static Color getMenuItemFg(UITheme t) {
return t == UITheme.DARK ? new Color(230, 230, 230) : new Color(30, 30, 30);
}
public static Color getMenuItemHoverBg() { return getMenuItemHoverBg(currentTheme); }
public static Color getMenuItemHoverBg(UITheme t) {
return t == UITheme.DARK ? new Color(55, 95, 160) : new Color(220, 232, 250);
}
public static Color getMenuItemHoverFg() { return getMenuItemHoverFg(currentTheme); }
public static Color getMenuItemHoverFg(UITheme t) {
return t == UITheme.DARK ? Color.WHITE : new Color(15, 15, 15);
}
public static Color getTabBg() { return getTabBg(currentTheme); }
public static Color getTabBg(UITheme t) {
return t == UITheme.DARK ? new Color(34, 34, 34) : new Color(230, 230, 232);
}
public static Color getTabFg() { return getTabFg(currentTheme); }
public static Color getTabFg(UITheme t) {
return t == UITheme.DARK ? new Color(185, 185, 185) : new Color(75, 75, 75);
}
public static Color getTabSelectedBg() { return getTabSelectedBg(currentTheme); }
public static Color getTabSelectedBg(UITheme t) {
return t == UITheme.DARK ? new Color(50, 50, 50) : Color.WHITE;
}
public static Color getTabSelectedFg() { return getTabSelectedFg(currentTheme); }
public static Color getTabSelectedFg(UITheme t) {
return t == UITheme.DARK ? Color.WHITE : new Color(20, 20, 20);
}
public static Color getTableHeaderBg() { return getTableHeaderBg(currentTheme); }
public static Color getTableHeaderBg(UITheme t) {
return t == UITheme.DARK ? new Color(46, 46, 46) : new Color(232, 232, 236);
}
public static Color getTableHeaderFg() { return getTableHeaderFg(currentTheme); }
public static Color getTableHeaderFg(UITheme t) {
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
}
public static Color getTableRowEven() { return getTableRowEven(currentTheme); }
public static Color getTableRowEven(UITheme t) {
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
}
public static Color getTableRowOdd() { return getTableRowOdd(currentTheme); }
public static Color getTableRowOdd(UITheme t) {
return t == UITheme.DARK ? new Color(33, 33, 33) : new Color(246, 247, 249);
}
public static Color getTableGrid() { return getTableGrid(currentTheme); }
public static Color getTableGrid(UITheme t) {
return t == UITheme.DARK ? new Color(55, 55, 55) : new Color(225, 225, 225);
}
public static Color getCodeAreaBg() { return getCodeAreaBg(currentTheme); }
public static Color getCodeAreaBg(UITheme t) {
return t == UITheme.DARK ? new Color(22, 22, 22) : new Color(252, 252, 252);
}
public static Color getCodeAreaFg() { return getCodeAreaFg(currentTheme); }
public static Color getCodeAreaFg(UITheme t) {
return t == UITheme.DARK ? new Color(80, 230, 80) : new Color(0, 120, 0);
}
public static Color getStatusBarBg() { return getStatusBarBg(currentTheme); }
public static Color getStatusBarBg(UITheme t) {
return t == UITheme.DARK ? new Color(20, 20, 20) : new Color(235, 235, 238);
}
public static Color getStatusBarBorder() { return getStatusBarBorder(currentTheme); }
public static Color getStatusBarBorder(UITheme t) {
return t == UITheme.DARK ? new Color(42, 42, 42) : new Color(210, 210, 215);
}
public static Color getOiaFgNormal() { return getOiaFgNormal(currentTheme); }
public static Color getOiaFgNormal(UITheme t) {
return t == UITheme.DARK ? new Color(50, 205, 50) : new Color(0, 130, 0);
}
public static Color getOiaFgDim() { return getOiaFgDim(currentTheme); }
public static Color getOiaFgDim(UITheme t) {
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(90, 90, 90);
}
public static Color getOiaFgAlert() { return getOiaFgAlert(currentTheme); }
public static Color getOiaFgAlert(UITheme t) {
return t == UITheme.DARK ? new Color(255, 80, 80) : new Color(190, 20, 20);
}
public static Color getOiaFgWarn() { return getOiaFgWarn(currentTheme); }
public static Color getOiaFgWarn(UITheme t) {
return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
}
// =========================================================================
// Button Variants & Color helpers
// =========================================================================
public enum ButtonVariant {
DEFAULT,
PRIMARY,
CANCEL,
DANGER,
ACCENT
}
public static Color getButtonBg(ButtonVariant variant) {
return getButtonBg(variant, currentTheme, false, false);
}
public static Color getButtonBg(ButtonVariant variant, UITheme t) {
return getButtonBg(variant, t, false, false);
}
public static Color getButtonBg(ButtonVariant variant, UITheme t, boolean hover, boolean pressed) {
switch (variant) {
case PRIMARY:
if (pressed) return new Color(25, 105, 35);
if (hover) return new Color(38, 145, 48);
return new Color(32, 128, 42);
case DANGER:
if (pressed) return new Color(125, 30, 30);
if (hover) return new Color(165, 45, 45);
return new Color(145, 38, 38);
case ACCENT:
if (pressed) return new Color(35, 80, 145);
if (hover) return new Color(55, 115, 195);
return new Color(45, 100, 175);
case CANCEL:
case DEFAULT:
default:
if (t == UITheme.DARK) {
if (pressed) return new Color(42, 42, 42);
if (hover) return new Color(68, 68, 68);
return new Color(56, 56, 56);
} else {
if (pressed) return new Color(210, 210, 212);
if (hover) return new Color(228, 228, 232);
return new Color(238, 238, 240);
}
}
}
public static Color getButtonFg(ButtonVariant variant, UITheme t) {
switch (variant) {
case PRIMARY:
case DANGER:
case ACCENT:
return Color.WHITE;
case CANCEL:
case DEFAULT:
default:
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
}
}
// =========================================================================
// UIManager Defaults Configuration
// =========================================================================
public static void applyUIManagerDefaults(UITheme theme) {
Color bgMain = getBgMain(theme);
Color bgPanel = getBgPanel(theme);
Color bgComp = getBgComponent(theme);
Color fgMain = getFgMain(theme);
Color fgMuted = getFgMuted(theme);
Color border = getBorder(theme);
Color selBg = getSelectionBg(theme);
Color selFg = getSelectionFg(theme);
UIManager.put("Panel.background", bgPanel);
UIManager.put("Panel.foreground", fgMain);
UIManager.put("Label.foreground", fgMain);
UIManager.put("TextField.background", bgComp);
UIManager.put("TextField.foreground", fgMain);
UIManager.put("TextField.caretForeground", fgMain);
UIManager.put("TextField.selectionBackground", selBg);
UIManager.put("TextField.selectionForeground", selFg);
UIManager.put("TextArea.background", bgComp);
UIManager.put("TextArea.foreground", fgMain);
UIManager.put("TextArea.caretForeground", fgMain);
UIManager.put("TextArea.selectionBackground", selBg);
UIManager.put("TextArea.selectionForeground", selFg);
UIManager.put("Button.background", getBgComponentLighter(theme));
UIManager.put("Button.foreground", fgMain);
UIManager.put("Button.select", selBg);
UIManager.put("ComboBox.background", bgComp);
UIManager.put("ComboBox.foreground", fgMain);
UIManager.put("ComboBox.selectionBackground", selBg);
UIManager.put("ComboBox.selectionForeground", selFg);
UIManager.put("Table.background", bgComp);
UIManager.put("Table.foreground", fgMain);
UIManager.put("Table.selectionBackground", selBg);
UIManager.put("Table.selectionForeground", selFg);
UIManager.put("Table.gridColor", getTableGrid(theme));
UIManager.put("TableHeader.background", getTableHeaderBg(theme));
UIManager.put("TableHeader.foreground", getTableHeaderFg(theme));
UIManager.put("ScrollPane.background", bgPanel);
UIManager.put("Viewport.background", bgComp);
UIManager.put("MenuBar.background", getMenuBarBg(theme));
UIManager.put("MenuBar.foreground", getMenuBarFg(theme));
UIManager.put("Menu.background", getMenuBarBg(theme));
UIManager.put("Menu.foreground", getMenuBarFg(theme));
UIManager.put("Menu.selectionBackground", selBg);
UIManager.put("Menu.selectionForeground", selFg);
UIManager.put("PopupMenu.background", getMenuPopupBg(theme));
UIManager.put("PopupMenu.foreground", getMenuItemFg(theme));
UIManager.put("MenuItem.background", getMenuPopupBg(theme));
UIManager.put("MenuItem.foreground", getMenuItemFg(theme));
UIManager.put("MenuItem.selectionBackground", selBg);
UIManager.put("MenuItem.selectionForeground", selFg);
UIManager.put("CheckBoxMenuItem.background", getMenuPopupBg(theme));
UIManager.put("CheckBoxMenuItem.foreground", getMenuItemFg(theme));
UIManager.put("CheckBoxMenuItem.selectionBackground", selBg);
UIManager.put("CheckBoxMenuItem.selectionForeground", selFg);
UIManager.put("RadioButtonMenuItem.background", getMenuPopupBg(theme));
UIManager.put("RadioButtonMenuItem.foreground", getMenuItemFg(theme));
UIManager.put("RadioButtonMenuItem.selectionBackground", selBg);
UIManager.put("RadioButtonMenuItem.selectionForeground", selFg);
UIManager.put("Separator.background", border);
UIManager.put("Separator.foreground", border);
UIManager.put("TabbedPane.background", getTabBg(theme));
UIManager.put("TabbedPane.foreground", getTabFg(theme));
UIManager.put("TabbedPane.selected", getTabSelectedBg(theme));
UIManager.put("TabbedPane.selectHighlight", getTabSelectedBg(theme));
UIManager.put("CheckBox.background", bgPanel);
UIManager.put("CheckBox.foreground", fgMain);
UIManager.put("RadioButton.background", bgPanel);
UIManager.put("RadioButton.foreground", fgMain);
UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain);
}
// =========================================================================
// Component Styling & Custom UI Delegates
// =========================================================================
/**
* Styles a button with custom rendering, ensuring consistent high contrast
* and proper background color across Windows, macOS, and Linux.
*/
public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null;
button.setUI(new StyledButtonUI(variant));
button.setFocusPainted(false);
button.setOpaque(false);
button.setContentAreaFilled(false);
button.setBorder(BorderFactory.createEmptyBorder(6, 14, 6, 14));
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return button;
}
public static JButton createStyledButton(String text, ButtonVariant variant, Runnable action) {
JButton btn = new JButton(text);
styleButton(btn, variant);
if (action != null) {
btn.addActionListener(e -> action.run());
}
return btn;
}
public static JTextField styleTextField(JTextField field) {
if (field == null) return null;
field.setBackground(getBgComponent());
field.setForeground(getFgMain());
field.setCaretColor(getFgMain());
field.setSelectionColor(getSelectionBg());
field.setSelectedTextColor(getSelectionFg());
field.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1, true),
new EmptyBorder(4, 8, 4, 8)));
return field;
}
public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null;
area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain());
area.setSelectionColor(getSelectionBg());
area.setSelectedTextColor(getSelectionFg());
return area;
}
public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null;
box.setBackground(getBgComponent());
box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setBorder(new EmptyBorder(4, 8, 4, 8));
if (isSelected) {
setBackground(getSelectionBg());
setForeground(getSelectionFg());
} else {
setBackground(getBgComponent());
setForeground(getFgMain());
}
return this;
}
});
return box;
}
public static JTable styleTable(JTable table) {
if (table == null) return null;
table.setBackground(getBgComponent());
table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid());
table.setRowHeight(24);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBorder(new EmptyBorder(2, 6, 2, 6));
if (isSel) {
setBackground(getSelectionBg());
setForeground(getSelectionFg());
} else {
setBackground(row % 2 == 0 ? getTableRowEven() : getTableRowOdd());
setForeground(getFgMain());
}
return this;
}
});
JTableHeader header = table.getTableHeader();
if (header != null) {
header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD));
header.setDefaultRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBackground(getTableHeaderBg());
setForeground(getTableHeaderFg());
setFont(getFont().deriveFont(Font.BOLD));
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6)));
return this;
}
});
}
return table;
}
public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI());
tp.setBackground(getBgMain());
tp.setForeground(getFgMain());
return tp;
}
public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null;
bar.setUI(new StyledMenuBarUI());
bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar;
}
public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null;
menu.setUI(new StyledMenuUI());
menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg());
menu.setOpaque(false);
return menu;
}
public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null;
item.setUI(new StyledMenuItemUI());
item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg());
return item;
}
public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI());
popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1),
new EmptyBorder(4, 0, 4, 0)));
return popup;
}
public static JScrollPane styleScrollPane(JScrollPane sp) {
if (sp == null) return null;
sp.setBackground(getBgPanel());
if (sp.getViewport() != null) {
sp.getViewport().setBackground(getBgComponent());
}
sp.setBorder(new LineBorder(getBorder(), 1));
return sp;
}
public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null;
sp.setBackground(getBgComponent());
sp.setForeground(getFgMain());
sp.setBorder(new LineBorder(getBorder(), 1));
JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setBackground(getBgComponent());
tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain());
tf.setBorder(new EmptyBorder(2, 4, 2, 4));
}
return sp;
}
public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null;
cb.setOpaque(false);
cb.setForeground(getFgMain());
cb.setFocusPainted(false);
return cb;
}
public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null;
rb.setOpaque(false);
rb.setForeground(getFgMain());
rb.setFocusPainted(false);
return rb;
}
public static Border createTitledBorder(String title) {
return BorderFactory.createTitledBorder(
new LineBorder(getBorder(), 1),
title,
TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12),
getFgMain());
}
// =========================================================================
// Recursive Component Styling
// =========================================================================
public static void applyTheme(Component comp) {
applyTheme(comp, currentTheme);
}
public static void applyTheme(Component comp, UITheme theme) {
if (comp == null) return;
// Skip swatch preview panels whose background is the user-configured color
if ("colorSwatch".equals(comp.getName())) {
return;
}
if (comp instanceof JMenuBar) {
styleMenuBar((JMenuBar) comp);
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
applyTheme(((JMenuBar) comp).getMenu(i), theme);
}
return;
}
if (comp instanceof JMenu) {
styleMenu((JMenu) comp);
JMenu menu = (JMenu) comp;
for (int i = 0; i < menu.getItemCount(); i++) {
JMenuItem item = menu.getItem(i);
if (item != null) applyTheme(item, theme);
}
return;
}
if (comp instanceof JMenuItem) {
styleMenuItem((JMenuItem) comp);
return;
}
if (comp instanceof JPopupMenu) {
stylePopupMenu((JPopupMenu) comp);
for (Component child : ((JPopupMenu) comp).getComponents()) {
applyTheme(child, theme);
}
return;
}
if (comp instanceof JTabbedPane) {
styleTabbedPane((JTabbedPane) comp);
JTabbedPane tp = (JTabbedPane) comp;
for (int i = 0; i < tp.getTabCount(); i++) {
applyTheme(tp.getComponentAt(i), theme);
}
return;
}
if (comp instanceof JTable) {
styleTable((JTable) comp);
return;
}
if (comp instanceof JScrollPane) {
styleScrollPane((JScrollPane) comp);
JScrollPane sp = (JScrollPane) comp;
if (sp.getViewport() != null) {
applyTheme(sp.getViewport().getView(), theme);
}
return;
}
if (comp instanceof JButton) {
JButton btn = (JButton) comp;
if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT);
}
return;
}
if (comp instanceof JComboBox) {
styleComboBox((JComboBox<?>) comp);
return;
}
if (comp instanceof JSpinner) {
styleSpinner((JSpinner) comp);
return;
}
if (comp instanceof JTextArea) {
styleTextArea((JTextArea) comp);
return;
}
if (comp instanceof JTextField) {
styleTextField((JTextField) comp);
return;
}
if (comp instanceof JCheckBox) {
styleCheckBox((JCheckBox) comp);
return;
}
if (comp instanceof JRadioButton) {
styleRadioButton((JRadioButton) comp);
return;
}
if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme));
return;
}
if (comp instanceof JPanel) {
JPanel panel = (JPanel) comp;
if (panel.isOpaque()) {
panel.setBackground(getBgPanel(theme));
}
panel.setForeground(getFgMain(theme));
Border b = panel.getBorder();
if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme));
tb.setBorder(new LineBorder(getBorder(theme), 1));
}
} else if (comp instanceof Container) {
comp.setBackground(getBgMain(theme));
comp.setForeground(getFgMain(theme));
}
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyTheme(child, theme);
}
}
}
public static void applyThemeToWindow(Window window) {
if (window == null) return;
window.setBackground(getBgMain());
if (window instanceof RootPaneContainer) {
RootPaneContainer rpc = (RootPaneContainer) window;
if (rpc.getContentPane() != null) {
rpc.getContentPane().setBackground(getBgMain());
applyTheme(rpc.getContentPane());
}
if (rpc.getRootPane() != null && rpc.getRootPane().getJMenuBar() != null) {
applyTheme(rpc.getRootPane().getJMenuBar());
}
}
window.repaint();
}
// =========================================================================
// Custom UI Implementations
// =========================================================================
public static class StyledButtonUI extends BasicButtonUI {
private final ButtonVariant variant;
private boolean hover = false;
public StyledButtonUI(ButtonVariant variant) {
this.variant = variant != null ? variant : ButtonVariant.DEFAULT;
}
@Override
public void installUI(JComponent c) {
super.installUI(c);
AbstractButton b = (AbstractButton) c;
b.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
hover = true;
b.repaint();
}
@Override
public void mouseExited(MouseEvent e) {
hover = false;
b.repaint();
}
});
}
@Override
public void paint(Graphics g, JComponent c) {
AbstractButton b = (AbstractButton) c;
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = c.getWidth();
int h = c.getHeight();
ButtonModel model = b.getModel();
boolean pressed = model.isArmed() && model.isPressed();
boolean enabled = b.isEnabled();
UITheme t = ThemeManager.getTheme();
Color bg = enabled ? ThemeManager.getButtonBg(variant, t, hover, pressed) : (t == UITheme.DARK ? new Color(40, 40, 40) : new Color(220, 220, 220));
Color fg = enabled ? ThemeManager.getButtonFg(variant, t) : ThemeManager.getFgDisabled(t);
// Background
g2.setColor(bg);
g2.fillRoundRect(0, 0, w, h, 6, 6);
// Border
if (variant == ButtonVariant.DEFAULT || variant == ButtonVariant.CANCEL) {
g2.setColor(ThemeManager.getBorder(t));
g2.drawRoundRect(0, 0, w - 1, h - 1, 6, 6);
}
// Text / Icon
FontMetrics fm = g2.getFontMetrics(b.getFont());
String text = b.getText();
if (text != null && !text.isEmpty()) {
g2.setColor(fg);
g2.setFont(b.getFont());
int tx = (w - fm.stringWidth(text)) / 2;
int ty = (h - fm.getHeight()) / 2 + fm.getAscent();
g2.drawString(text, tx, ty);
}
g2.dispose();
}
}
public static class StyledMenuBarUI extends BasicMenuBarUI {
@Override
public void paint(Graphics g, JComponent c) {
g.setColor(ThemeManager.getMenuBarBg());
g.fillRect(0, 0, c.getWidth(), c.getHeight());
}
}
public static class StyledMenuUI extends BasicMenuUI {
@Override
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getMenuItemHoverBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
} else {
g.setColor(ThemeManager.getMenuBarBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
}
}
@Override
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getMenuItemHoverFg());
} else {
g.setColor(ThemeManager.getMenuBarFg());
}
super.paintText(g, menuItem, textRect, text);
}
}
public static class StyledMenuItemUI extends BasicMenuItemUI {
@Override
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getSelectionBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
} else {
g.setColor(ThemeManager.getMenuPopupBg());
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
}
}
@Override
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
ButtonModel model = menuItem.getModel();
if (model.isArmed() || model.isSelected()) {
g.setColor(ThemeManager.getSelectionFg());
} else {
g.setColor(ThemeManager.getMenuItemFg());
}
super.paintText(g, menuItem, textRect, text);
}
}
public static class StyledPopupMenuUI extends BasicPopupMenuUI {
@Override
public void paint(Graphics g, JComponent c) {
g.setColor(ThemeManager.getMenuPopupBg());
g.fillRect(0, 0, c.getWidth(), c.getHeight());
}
}
public static class StyledTabbedPaneUI extends BasicTabbedPaneUI {
@Override
protected void installDefaults() {
super.installDefaults();
tabInsets = new Insets(6, 16, 6, 16);
selectedTabPadInsets = new Insets(2, 2, 2, 2);
tabAreaInsets = new Insets(4, 4, 0, 4);
}
@Override
protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(isSelected ? ThemeManager.getTabSelectedBg() : ThemeManager.getTabBg());
g2.fillRoundRect(x, y, w, h + 4, 6, 6);
g2.setColor(ThemeManager.getBorder());
g2.drawRoundRect(x, y, w - 1, h + 4, 6, 6);
g2.dispose();
}
@Override
protected void paintText(Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected) {
g.setFont(font);
g.setColor(isSelected ? ThemeManager.getTabSelectedFg() : ThemeManager.getTabFg());
g.drawString(title, textRect.x, textRect.y + metrics.getAscent());
}
@Override
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
int width = tabPane.getWidth();
int height = tabPane.getHeight();
Insets insets = tabPane.getInsets();
int x = insets.left;
int y = insets.top + calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
int w = width - insets.right - insets.left;
int h = height - insets.top - insets.bottom - calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
g.setColor(ThemeManager.getTabSelectedBg());
g.fillRect(x, y, w, h);
g.setColor(ThemeManager.getBorder());
g.drawRect(x, y, w - 1, h - 1);
}
}
}
@@ -0,0 +1,33 @@
package haus.nightmare.j3270.ui;
/**
* Supported UI themes for the j3270 Java desktop interface.
*/
public enum UITheme {
DARK("Dark Mode"),
LIGHT("Light Mode");
private final String displayName;
UITheme(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
@Override
public String toString() {
return displayName;
}
public static UITheme fromString(String name) {
if (name == null) return DARK;
String s = name.trim().toUpperCase();
if ("LIGHT".equals(s) || "LIGHT MODE".equals(s) || "LIGHT_MODE".equals(s)) {
return LIGHT;
}
return DARK;
}
}
@@ -26,25 +26,24 @@ public class UntrustedCertificateDialog extends JDialog {
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) { private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
JPanel mainPanel = new JPanel(new BorderLayout(12, 12)); JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
// Header // Header
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setBackground(new Color(30, 30, 30)); headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️"); JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
headerPanel.add(iconLabel); headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setBackground(new Color(30, 30, 30)); titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
titleLabel.setForeground(new Color(255, 180, 80)); titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel); titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
subtitleLabel.setForeground(new Color(180, 180, 180)); subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel); titleBox.add(subtitleLabel);
headerPanel.add(titleBox); headerPanel.add(titleBox);
mainPanel.add(headerPanel, BorderLayout.NORTH); mainPanel.add(headerPanel, BorderLayout.NORTH);
@@ -71,30 +70,27 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString()); JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false); detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
detailsArea.setBackground(new Color(20, 20, 20)); ThemeManager.styleTextArea(detailsArea);
detailsArea.setForeground(new Color(210, 210, 210));
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane scrollPane = new JScrollPane(detailsArea); JScrollPane scrollPane = new JScrollPane(detailsArea);
scrollPane.setPreferredSize(new Dimension(520, 260)); scrollPane.setPreferredSize(new Dimension(520, 260));
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60))); ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Buttons // Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setBackground(new Color(30, 30, 30)); buttonPanel.setOpaque(false);
JButton cancelBtn = new JButton("Cancel Connection"); JButton cancelBtn = new JButton("Cancel Connection");
cancelBtn.setBackground(new Color(60, 60, 60)); ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setForeground(new Color(220, 220, 220));
cancelBtn.addActionListener(e -> { cancelBtn.addActionListener(e -> {
accepted = false; accepted = false;
dispose(); dispose();
}); });
JButton trustBtn = new JButton("Connect Anyway"); JButton trustBtn = new JButton("Connect Anyway");
trustBtn.setBackground(new Color(180, 100, 40)); ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setForeground(Color.WHITE);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
trustBtn.addActionListener(e -> { trustBtn.addActionListener(e -> {
accepted = true; accepted = true;
@@ -106,6 +102,7 @@ public class UntrustedCertificateDialog extends JDialog {
mainPanel.add(buttonPanel, BorderLayout.SOUTH); mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
getRootPane().setDefaultButton(trustBtn); getRootPane().setDefaultButton(trustBtn);
} }
@@ -0,0 +1,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 @Test
public void testStatusBarDoesNotContainLightPenButton() { public void testStatusBarDoesNotContainLightPenButton() {
StatusBar statusBar = new StatusBar(); try {
StatusBar statusBar = new StatusBar();
// Ensure no JButton exists in StatusBar components (lightpen button removed) // Ensure no JButton exists in StatusBar components (lightpen button removed)
for (Component comp : statusBar.getComponents()) { for (Component comp : statusBar.getComponents()) {
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)"); assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
}
} catch (HeadlessException e) {
// Ignored in headless environments
} }
} }
@Test @Test
public void testStatusBarUpdatesWithClient() { public void testStatusBarUpdatesWithClient() {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false); try {
config.setLuName("TSU001"); ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
config.setCodePage("1047"); config.setLuName("TSU001");
Telnet3270Client client = new Telnet3270Client(config); config.setCodePage("1047");
Telnet3270Client client = new Telnet3270Client(config);
StatusBar statusBar = new StatusBar(); StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null); statusBar.setClient(client, null);
statusBar.updateStatus(); statusBar.updateStatus();
// Check labels // Check labels
boolean foundCodePage = false; boolean foundCodePage = false;
boolean foundModel = false; boolean foundModel = false;
for (Component comp : statusBar.getComponents()) { for (Component comp : statusBar.getComponents()) {
if (comp instanceof JLabel) { if (comp instanceof JLabel) {
JLabel label = (JLabel) comp; JLabel label = (JLabel) comp;
if ("CP1047".equals(label.getText())) { if ("CP1047".equals(label.getText())) {
foundCodePage = true; foundCodePage = true;
} }
if (label.getText() != null && label.getText().contains("3279-4")) { if (label.getText() != null && label.getText().contains("3279-4")) {
foundModel = true; foundModel = true;
}
} }
} }
}
assertTrue(foundCodePage, "Should display CP1047"); assertTrue(foundCodePage, "Should display CP1047");
assertTrue(foundModel, "Should display IBM-3279-4 model info"); 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 class ConnectionConfig {
public enum ProxyType {
NONE, HTTP, SOCKS4, SOCKS5
}
private String host; private String host;
private int port = 23; private int port = 23;
private TerminalModel model = TerminalModel.IBM_3279_4; private TerminalModel model = TerminalModel.IBM_3279_4;
@@ -28,6 +32,21 @@ public class ConnectionConfig {
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH; private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
private String codePage = "037"; private String codePage = "037";
private String associatedPrinterLu = null; private String associatedPrinterLu = null;
private boolean nvtLocalEcho = false;
// Proxy configuration
private ProxyType proxyType = ProxyType.NONE;
private String proxyHost = null;
private int proxyPort = 0;
private String proxyUsername = null;
private String proxyPassword = null;
// STARTTLS (Telnet Option 46) dynamic socket elevation
private boolean startTlsEnabled = true;
// RFC 1572 / RFC 2877 Environment variables (Express Logon)
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
public ConnectionConfig() {} public ConnectionConfig() {}
@@ -101,6 +120,9 @@ public class ConnectionConfig {
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037"; this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
} }
public boolean isNvtLocalEcho() { return nvtLocalEcho; }
public void setNvtLocalEcho(boolean nvtLocalEcho) { this.nvtLocalEcho = nvtLocalEcho; }
public String getTerminalName() { return terminalName; } public String getTerminalName() { return terminalName; }
public void setTerminalName(String name) { this.terminalName = name; } public void setTerminalName(String name) { this.terminalName = name; }
@@ -134,9 +156,58 @@ public class ConnectionConfig {
this.dynamicCols = cols; this.dynamicCols = cols;
} }
public ProxyType getProxyType() { return proxyType; }
public void setProxyType(ProxyType proxyType) { this.proxyType = proxyType != null ? proxyType : ProxyType.NONE; }
public String getProxyHost() { return proxyHost; }
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
public int getProxyPort() { return proxyPort; }
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
public String getProxyUsername() { return proxyUsername; }
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
public String getProxyPassword() { return proxyPassword; }
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
public void setProxy(ProxyType type, String host, int port, String username, String password) {
this.proxyType = type != null ? type : ProxyType.NONE;
this.proxyHost = host;
this.proxyPort = port;
this.proxyUsername = username;
this.proxyPassword = password;
}
public boolean isStartTlsEnabled() { return startTlsEnabled; }
public void setStartTlsEnabled(boolean enabled) { this.startTlsEnabled = enabled; }
public java.util.Map<String, String> getEnvironmentVariables() { return environmentVariables; }
public void setEnvironmentVariables(java.util.Map<String, String> vars) {
this.environmentVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
}
public void setEnvironmentVariable(String name, String value) {
if (name != null) {
if (value != null) this.environmentVariables.put(name, value);
else this.environmentVariables.remove(name);
}
}
public java.util.Map<String, String> getUserVariables() { return userVariables; }
public void setUserVariables(java.util.Map<String, String> vars) {
this.userVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
}
public void setUserVariable(String name, String value) {
if (name != null) {
if (value != null) this.userVariables.put(name, value);
else this.userVariables.remove(name);
}
}
/** /**
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"), * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats. * plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
* or standard "host:port" formats.
*/ */
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) { public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
if (hostStr == null || hostStr.trim().isEmpty()) { if (hostStr == null || hostStr.trim().isEmpty()) {
@@ -146,6 +217,50 @@ public class ConnectionConfig {
boolean tls = false; boolean tls = false;
boolean tn3270e = true; boolean tn3270e = true;
// Parse --proxy=<url> or -proxy=<url> flags
ProxyType pType = ProxyType.NONE;
String pHost = null;
int pPort = 0;
String pUser = null;
String pPass = null;
String[] tokens = s.split("\\s+");
StringBuilder remaining = new StringBuilder();
for (String tok : tokens) {
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
try {
java.net.URI uri = new java.net.URI(proxyUrl);
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
if (scheme.equals("http") || scheme.equals("https")) {
pType = ProxyType.HTTP;
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
pType = ProxyType.SOCKS4;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
pType = ProxyType.SOCKS5;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
}
pHost = uri.getHost();
String userInfo = uri.getUserInfo();
if (userInfo != null) {
int colon = userInfo.indexOf(':');
if (colon >= 0) {
pUser = userInfo.substring(0, colon);
pPass = userInfo.substring(colon + 1);
} else {
pUser = userInfo;
}
}
} catch (Exception ignored) {}
} else {
if (remaining.length() > 0) remaining.append(" ");
remaining.append(tok);
}
}
s = remaining.toString().trim();
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port") // Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
boolean prefixFound = true; boolean prefixFound = true;
while (prefixFound) { while (prefixFound) {
@@ -200,6 +315,9 @@ public class ConnectionConfig {
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
config.setUseTls(tls); config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e); config.setTn3270eEnabled(tn3270e);
if (pType != ProxyType.NONE && pHost != null) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
return config; return config;
} }
@@ -59,4 +59,32 @@ public enum ConnectionState {
public boolean isFullSession() { public boolean isFullSession() {
return isNvt() || is3270(); 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.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator); this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm); this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
this.inputProcessor.setOIA(oia);
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator); this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection // Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
@@ -62,11 +63,21 @@ public class Telnet3270Client {
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane()); inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder()); 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() { addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
@Override public void onScreenUpdated() { xfer.onScreenUpdated(); } @Override public void onScreenUpdated() {
@Override public void onScreenSizeChanged(int rows, int cols) {} xfer.onScreenUpdated();
@Override public void onSoundAlarm() {} 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(); 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. * Disconnect from the host.
*/ */
@@ -100,6 +157,35 @@ public class Telnet3270Client {
fsm.onDisconnect(); 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). * Check if connected (any state past TCP pending).
*/ */
@@ -113,11 +199,28 @@ public class Telnet3270Client {
fsm.addConnectionListener(l); fsm.addConnectionListener(l);
} }
public void removeConnectionListener(ConnectionListener l) {
fsm.removeConnectionListener(l);
}
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
fsm.addScreenUpdateListener(l); fsm.addScreenUpdateListener(l);
dsProcessor.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 ========== // ========== Screen access ==========
/** Get the screen buffer for rendering. */ /** Get the screen buffer for rendering. */
@@ -189,11 +292,48 @@ public class Telnet3270Client {
fsm.sendNVTString(s); 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. */ /** Type a character at the cursor position. */
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); } public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
/** Process a character at the current cursor position. */
public void processChar(char ch) { inputProcessor.processChar(ch); }
/** Process a character with explicit position and insert mode flag. */
public void processChar(char ch, int pos, boolean insert) { inputProcessor.processChar(ch, pos, insert); }
/** Type a string at the cursor position. */ /** Type a string at the cursor position. */
public void typeString(String s) { public void typeString(String s) {
for (char ch : s.toCharArray()) { for (char ch : s.toCharArray()) {
@@ -210,6 +350,7 @@ public class Telnet3270Client {
public void sendEnter() { public void sendEnter() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER); inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
} }
public void processEnter() { inputProcessor.processEnter(); }
/** Send a PF key (1-24). */ /** Send a PF key (1-24). */
public void sendPF(int number) { public void sendPF(int number) {
@@ -231,6 +372,7 @@ public class Telnet3270Client {
} }
inputProcessor.sendAid(aid); inputProcessor.sendAid(aid);
} }
public void processPF(int number) { inputProcessor.processPF(number); }
/** Send a PA key (1-3). */ /** Send a PA key (1-3). */
public void sendPA(int number) { public void sendPA(int number) {
@@ -243,44 +385,104 @@ public class Telnet3270Client {
} }
inputProcessor.sendAid(aid); inputProcessor.sendAid(aid);
} }
public void processPA(int number) { inputProcessor.processPA(number); }
/** Send Clear key. */ /** Send Clear key. */
public void sendClear() { public void sendClear() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR); inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
} }
public void processClear() { inputProcessor.processClear(); }
/** Move cursor up. */ /** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); } public void cursorUp() { inputProcessor.cursorUp(); }
public void processCursorUp() { inputProcessor.processCursorUp(); }
/** Move cursor down. */ /** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); } public void cursorDown() { inputProcessor.cursorDown(); }
public void processCursorDown() { inputProcessor.processCursorDown(); }
/** Move cursor left. */ /** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); } public void cursorLeft() { inputProcessor.cursorLeft(); }
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
/** Move cursor right. */ /** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); } public void cursorRight() { inputProcessor.cursorRight(); }
public void processCursorRight() { inputProcessor.processCursorRight(); }
/** Move cursor to home position. */ /** Move cursor to home position. */
public void cursorHome() { inputProcessor.cursorHome(); } public void cursorHome() { inputProcessor.cursorHome(); }
public void processHome() { inputProcessor.processHome(); }
/** Tab to next unprotected field. */ /** Tab to next unprotected field. */
public void tab() { inputProcessor.tab(); } public void tab() { inputProcessor.tab(); }
public void processTab() { inputProcessor.processTab(); }
/** Back-tab to previous unprotected field. */ /** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); } public void backTab() { inputProcessor.backTab(); }
public void processBackTab() { inputProcessor.processBackTab(); }
/** Move cursor to next line. */ /** Move cursor to next line. */
public void newline() { inputProcessor.newline(); } public void newline() { inputProcessor.newline(); }
public void processNewline() { inputProcessor.processNewline(); }
/** Delete character under cursor. */
public void deleteChar() { inputProcessor.deleteChar(); }
public void processDelete() { inputProcessor.processDelete(); }
/** Backspace character before cursor. */
public void backspace() { inputProcessor.backspace(); }
public void processBackspace() { inputProcessor.processBackspace(); }
/** Erase to end of field. */
public void eraseEof() { inputProcessor.eraseEof(); }
public void processEraseEOF() { inputProcessor.processEraseEOF(); }
/** Erase all unprotected fields. */ /** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); } public void eraseInput() { inputProcessor.eraseInput(); }
public void processEraseInput() { inputProcessor.processEraseInput(); }
/** Insert Duplicate order. */ /** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); } public void dup() { inputProcessor.dup(); }
public void processDup() { inputProcessor.processDup(); }
/** Insert Field Mark order. */ /** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); } public void fieldMark() { inputProcessor.fieldMark(); }
public void processFieldMark() { inputProcessor.processFieldMark(); }
/** Toggle Insert Mode. */
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
/** Move word left. */
public void processWordLeft() { inputProcessor.processWordLeft(); }
/** Move word right. */
public void processWordRight() { inputProcessor.processWordRight(); }
/** Move to field end. */
public void processFieldEnd() { inputProcessor.processFieldEnd(); }
/** Attention key. */ /** Attention key. */
public void attn() { inputProcessor.attn(); } public void attn() { inputProcessor.attn(); }
public void processAttn() { inputProcessor.processAttn(); }
/** SysReq key. */ /** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); } public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */ public void processSysReq() { inputProcessor.processSysReq(); }
public void reset() { inputProcessor.reset(); }
/** Reset (unlock keyboard, reset OIA). */
public void reset() { inputProcessor.reset(); }
public void processReset() { inputProcessor.processReset(); }
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */ /** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); } public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
/** Trigger Light Pen selection at the specified screen address. */ public boolean processCurSel() { return inputProcessor.processCurSel(); }
public boolean processCursorSelect() { return inputProcessor.processCursorSelect(); }
/** Trigger Light Pen selection at the specified screen address or cursor position. */
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); } public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
public boolean processLightPen() { return inputProcessor.processLightPen(); }
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() { public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.getProgramSymbolManager(); return dsProcessor.getProgramSymbolManager();
@@ -106,6 +106,19 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override @Override
public String ebcdicToString(byte[] ebcdic, int offset, int length) { 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 ""; if (ebcdic == null || length <= 0) return "";
StringBuilder sb = new StringBuilder(length); StringBuilder sb = new StringBuilder(length);
@@ -117,9 +130,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int b = ebcdic[i] & 0xFF; int b = ebcdic[i] & 0xFF;
if (b == SO) { if (b == SO) {
inDBCS = true; inDBCS = true;
if (preserveSOSI) {
sb.append((char) SO);
}
i++; i++;
} else if (b == SI) { } else if (b == SI) {
inDBCS = false; inDBCS = false;
if (preserveSOSI) {
sb.append((char) SI);
}
i++; i++;
} else if (inDBCS) { } else if (inDBCS) {
if (i + 1 < end) { if (i + 1 < end) {
@@ -127,13 +146,22 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
if (b2 == SI) { if (b2 == SI) {
// Orphaned single byte before SI // Orphaned single byte before SI
inDBCS = false; inDBCS = false;
if (preserveSOSI) {
sb.append(ebcdicToUnicode(b));
sb.append((char) SI);
}
i += 2; i += 2;
} else if (b2 == SO) {
// Unmatched byte followed by another SO
sb.append(ebcdicToUnicode(b));
i++;
} else { } else {
sb.append(dbcsToUnicode(b, b2)); sb.append(dbcsToUnicode(b, b2));
i += 2; i += 2;
} }
} else { } else {
// Trailing byte // Trailing single byte inside unclosed DBCS shift
sb.append(ebcdicToUnicode(b));
i++; i++;
} }
} else { } else {
@@ -147,14 +175,39 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
@Override @Override
public byte[] stringToEbcdic(String s) { 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]; if (s == null || s.isEmpty()) return new byte[0];
// Manual state machine encoding
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2); ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
boolean inDBCS = false; boolean inDBCS = false;
for (int i = 0; i < s.length(); i++) { for (int i = 0; i < s.length(); i++) {
char c = s.charAt(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); int dbcsCode = unicodeToDbcs(c);
if (dbcsCode >= 0) { if (dbcsCode >= 0) {
@@ -175,10 +228,26 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
} }
} }
// Auto-close unclosed DBCS shift sequence
if (inDBCS) { if (inDBCS) {
out.write(SI); out.write(SI);
} }
return out.toByteArray(); 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 Cp937.Cp1371());
register(new Cp933()); register(new Cp933());
// Register Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS)
registerExtendedCodepages();
// Setup common aliases // Setup common aliases
addAlias("us", "037"); addAlias("us", "037");
addAlias("usa", "037"); addAlias("usa", "037");
@@ -218,6 +221,100 @@ public class CodePageRegistry {
return CODE_PAGES.get("037"); 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. * 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; 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). * EBCDIC ↔ Unicode character translator conforming to IBM Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS). * 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). * Default: Code Page 037 (US/Canada EBCDIC).
*/ */
public class EbcdicTranslator { public class EbcdicTranslator {
@@ -15,6 +18,8 @@ public class EbcdicTranslator {
public static final int[] CP037_TO_UNICODE = Cp037.MAPPING; public static final int[] CP037_TO_UNICODE = Cp037.MAPPING;
private CodePage activeCodePage; private CodePage activeCodePage;
private final Map<Integer, Character> customEbcdicToUnicode = new ConcurrentHashMap<>();
private final Map<Character, Integer> customUnicodeToEbcdic = new ConcurrentHashMap<>();
public EbcdicTranslator() { public EbcdicTranslator() {
this("037"); this("037");
@@ -81,6 +86,111 @@ public class EbcdicTranslator {
return activeCodePage.isDBCS(); return activeCodePage.isDBCS();
} }
public synchronized boolean isDBCS() {
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. * Translate double-byte EBCDIC pair (b1, b2) to Unicode.
*/ */
@@ -96,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) { 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);
} }
/** /**
@@ -117,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. * Returns -1 if the character cannot be mapped.
*/ */
public int unicodeToEbcdic(char unicode) { public int unicodeToEbcdic(char unicode) {
Integer custom = customUnicodeToEbcdic.get(unicode);
if (custom != null) {
return custom;
}
return activeCodePage.unicodeToEbcdic(unicode); return activeCodePage.unicodeToEbcdic(unicode);
} }
@@ -128,20 +247,37 @@ public class EbcdicTranslator {
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable. * Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/ */
public byte unicodeToEbcdicSafe(char unicode) { 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) { 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); 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) { 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); return activeCodePage.stringToEbcdic(s);
} }
@@ -160,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) { public char mapAPL(int ebcdicCodePoint) {
switch (ec & 0xFF) { switch (ebcdicCodePoint & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL) // Standard Box-Drawing Lines, Corners, T-Junctions, and Crosses
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─' case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│' case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌' case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
@@ -177,7 +314,7 @@ public class EbcdicTranslator {
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴' case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼' 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 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥' case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠' case 0xBE: return '\u2260'; // Not equal '≠'
@@ -189,12 +326,66 @@ public class EbcdicTranslator {
case 0xB1: return '\u00B1'; // Plus-minus '±' case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²' case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³' case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯' case 0xAF: return '\u00AF'; // Overbar / High Minus '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω' case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ' case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬' case 0x5F: return '\u00AC'; // Not sign '¬'
default: return ebcdicToUnicode(ec & 0xFF); // 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); screenListeners.add(l);
} }
public void removeScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.remove(l);
}
/** /**
* Process a 3270 data stream record. * Process a 3270 data stream record.
* *
@@ -144,6 +148,9 @@ public class DataStreamProcessor {
int oldCols = screen.getCols(); int oldCols = screen.getCols();
screen.erase(false); screen.erase(false);
graphicsPlane.clear(); graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, offset, length, true); processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) { if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged(); notifyScreenSizeChanged();
@@ -161,6 +168,9 @@ public class DataStreamProcessor {
int oldCols = screen.getCols(); int oldCols = screen.getCols();
screen.erase(true); screen.erase(true);
graphicsPlane.clear(); graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, offset, length, true); processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) { if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged(); notifyScreenSizeChanged();
@@ -510,11 +520,14 @@ public class DataStreamProcessor {
// Handle GE (graphic escape) prefix // Handle GE (graphic escape) prefix
byte fillCs = currentCs; byte fillCs = currentCs;
char fillUcs4 = 0;
if (pos + 4 < end && fillChar == ORDER_GE) { if (pos + 4 < end && fillChar == ORDER_GE) {
fillChar = data[pos + 4] & 0xFF; fillChar = data[pos + 4] & 0xFF;
fillCs = CS_GE; fillCs = CS_GE;
fillUcs4 = (translator != null) ? translator.mapAPL(fillChar) : (char) fillChar;
pos += 5; pos += 5;
} else { } else {
fillUcs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar) : (char) fillChar;
pos += 4; pos += 4;
} }
@@ -523,6 +536,7 @@ public class DataStreamProcessor {
ExtendedAttribute ea = screen.getCell(baddr); ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar; ea.ec = (byte) fillChar;
ea.ucs4 = fillUcs4;
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
@@ -575,6 +589,7 @@ public class DataStreamProcessor {
ea.fa = 0; // Destroy previous field attribute if any ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) geChar; ea.ec = (byte) geChar;
ea.cs = CS_GE; ea.cs = CS_GE;
ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar;
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
@@ -680,7 +695,7 @@ public class DataStreamProcessor {
// ========== Read Buffer ========== // ========== Read Buffer ==========
private void processReadBuffer() { public void processReadBuffer() {
outputPos = 0; outputPos = 0;
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
@@ -780,11 +795,21 @@ public class DataStreamProcessor {
// ========== Read Modified ========== // ========== Read Modified ==========
private void processReadModified(boolean all) { public void processReadModified(boolean all) {
if (ftDft != null && ftDft.readModified()) { if (ftDft != null && ftDft.readModified()) {
return; return;
} }
if (inputProcessor != null) {
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
inputProcessor.setLastAid(AID_NO);
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
if (outputSender != null) {
outputSender.send3270Data(data);
}
return;
}
outputPos = 0; outputPos = 0;
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
@@ -843,10 +868,11 @@ public class DataStreamProcessor {
sendOutput(); sendOutput();
} }
// ========== Write Structured Field ========== public void processWriteStructuredField(byte[] data, int offset, int length) {
if (data == null || length <= 0)
private void processWriteStructuredField(byte[] data, int offset, int length) { return;
int pos = offset + 1; // Skip WSF command byte int firstByte = data[offset] & 0xFF;
int pos = (firstByte == CMD_WSF || firstByte == SNA_CMD_WSF) ? offset + 1 : offset;
int end = offset + length; int end = offset + length;
while (pos < end) { while (pos < end) {
@@ -881,10 +907,19 @@ public class DataStreamProcessor {
case SF_ACTIVATE_PART: case SF_ACTIVATE_PART:
processActivatePartition(data, pos, fieldLen); processActivatePartition(data, pos, fieldLen);
break; break;
case SF_OUTBOUND_DS: case SF_SET_WINDOW: // 0x0F: Set Window / Modify Partition / 3270 Graphics Viewport
processSFSetWindow(data, pos, fieldLen);
break;
case SF_3270_GRAPHICS: // 0x20: 3270 Graphics / Object Control
processSFObjectControl(data, pos, fieldLen);
break;
case SF_DOCUMENT_DATA: // 0x24: Document Data / embedded SCS / Object Control
processSFDocumentData(data, pos, fieldLen);
break;
case SF_OUTBOUND_DS: // 0x40
processOutbound3270DS(data, pos, fieldLen); processOutbound3270DS(data, pos, fieldLen);
break; break;
case SF_TRANSFER_DATA: case SF_TRANSFER_DATA: // 0xD0
if (ftDft != null) { if (ftDft != null) {
ftDft.processStructuredField(data, pos, fieldLen); ftDft.processStructuredField(data, pos, fieldLen);
} else { } else {
@@ -898,95 +933,7 @@ public class DataStreamProcessor {
programSymbolManager.loadps(psData); programSymbolManager.loadps(psData);
} }
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
if (fieldLen >= 4) {
int sfSubId = data[pos + 3] & 0xFF;
switch (sfSubId) {
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
if (fieldLen > 4) {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, pos + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
StringBuilder hexDump = new StringBuilder();
for (int i = 0; i < Math.min(32, fieldLen); i++) {
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
}
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
currentGocaSubtype = sfSubId;
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x00) { // SPAN_MIDDLE
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x40) { // SPAN_LAST
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
}
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
gocaDecoder.decodeStream(data, orderOffset, orderLen);
}
notifyScreenUpdated();
}
break;
}
default:
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
break;
}
}
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
notifyScreenUpdated();
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) { if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
@@ -1020,7 +967,7 @@ public class DataStreamProcessor {
switch (type) { switch (type) {
case SF_RP_QUERY: case SF_RP_QUERY:
log.info("ReadPartition Query — sending all query replies"); log.info("ReadPartition Query — sending base query replies");
graphicsPlane.clear(); graphicsPlane.clear();
gocaDecoder.resetDefaults(); gocaDecoder.resetDefaults();
sendAllQueryReplies(); sendAllQueryReplies();
@@ -1029,7 +976,9 @@ public class DataStreamProcessor {
if (fieldLen >= 6) { if (fieldLen >= 6) {
int listType = data[offset + 5] & 0xFF; int listType = data[offset + 5] & 0xFF;
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType)); log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
if (listType == SF_RPQ_ALL || listType == SF_RPQ_EQUIV) { if (listType == SF_RPQ_ALL) {
sendCompleteQueryReplies();
} else if (listType == SF_RPQ_EQUIV) {
sendAllQueryReplies(); sendAllQueryReplies();
} else if (listType == SF_RPQ_LIST) { } else if (listType == SF_RPQ_LIST) {
// Send only requested query replies // Send only requested query replies
@@ -1071,7 +1020,22 @@ public class DataStreamProcessor {
if ((i + 1) % 32 == 0) if ((i + 1) % 32 == 0)
sb.append("\n "); sb.append("\n ");
} }
log.warning(">>> SENDING Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim()); log.warning(">>> SENDING Base Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
}
private void sendCompleteQueryReplies() {
byte[] qr = qrBuilder.buildCompleteQueryReplies(screen.getMaxCols(), screen.getMaxRows(),
screen.getMaxCols() * screen.getMaxRows());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < qr.length; i++) {
sb.append(String.format("%02x ", qr[i] & 0xFF));
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.warning(">>> SENDING Complete Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) { if (outputSender != null) {
outputSender.send3270Data(qr); outputSender.send3270Data(qr);
} }
@@ -1257,11 +1221,275 @@ public class DataStreamProcessor {
return pid >= 0 && pid <= 255; return pid >= 0 && pid <= 255;
} }
// ========== Missing Structured Field Handlers (Phase 2) ==========
public void processSFSetWindow(byte[] data, int offset, int fieldLen) {
if (fieldLen >= 4) {
int sfSubId = data[offset + 3] & 0xFF;
switch (sfSubId) {
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
if (fieldLen > 4) {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, offset + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[offset + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
currentGocaSubtype = sfSubId;
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x00) { // SPAN_MIDDLE
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x40) { // SPAN_LAST
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
}
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
gocaDecoder.decodeStream(data, orderOffset, orderLen);
}
notifyScreenUpdated();
}
break;
}
default:
if (fieldLen >= 11) {
int xMin = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
int yMin = ((data[offset + 5] & 0xFF) << 8) | (data[offset + 6] & 0xFF);
int xMax = ((data[offset + 7] & 0xFF) << 8) | (data[offset + 8] & 0xFF);
int yMax = ((data[offset + 9] & 0xFF) << 8) | (data[offset + 10] & 0xFF);
graphicsPlane.setViewingWindow(xMin, yMin, xMax, yMax);
}
log.fine("SF 0x0F Set Window processed (len=" + fieldLen + ")");
break;
}
}
}
public void processSFSetWindow(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFSetWindow(sf, off, sf.length - off);
}
}
public void processSFObjectControl(byte[] data, int offset, int fieldLen) {
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, offset + 3, fieldLen - 3);
notifyScreenUpdated();
}
}
public void processSFObjectControl(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFObjectControl(sf, off, sf.length - off);
}
}
public void processSFDocumentData(byte[] data, int offset, int fieldLen) {
if (fieldLen > 3) {
if (embeddedScsProcessor != null) {
embeddedScsProcessor.processHostData(data, offset + 3, fieldLen - 3);
}
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, offset + 3, fieldLen - 3);
notifyScreenUpdated();
}
}
public void processSFDocumentData(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFDocumentData(sf, off, sf.length - off);
}
}
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) { public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
if (fieldLen > 5) { if (fieldLen >= 4) {
int pid = data[offset + 3] & 0xFF; int pid = data[offset + 3] & 0xFF;
screen.setActivePartition(pid); screen.setActivePartition(pid);
processRecord(data, offset + 4, fieldLen - 4, false); if (fieldLen > 4) {
processRecord(data, offset + 4, fieldLen - 4, false);
}
}
}
public void processOutbound3270DS(byte[] sf) {
if (sf != null && sf.length >= 4) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processOutbound3270DS(sf, off, sf.length - off);
}
}
// ========== Convenience Overloads ==========
public void processRecord(byte[] data) {
if (data != null && data.length > 0) {
processRecord(data, 0, data.length, false);
}
}
public void processWrite(byte[] data) {
if (data != null && data.length > 0) {
processRecord(data, 0, data.length, false);
}
}
public void processEraseWrite(byte[] data) {
if (data != null && data.length > 0) {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(false);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, 0, data.length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
}
public void processEraseWriteAlternate(byte[] data) {
if (data != null && data.length > 0) {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(true);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, 0, data.length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
}
public void processEraseAllUnprotected() {
synchronized (screen.getRenderLock()) {
screen.eraseAllUnprotected();
}
}
public void processReadModified() {
processReadModified(false);
}
public void processReadModifiedAll() {
processReadModified(true);
}
public void processWriteStructuredField(byte[] data) {
if (data != null && data.length > 0) {
processWriteStructuredField(data, 0, data.length);
}
}
public void processSFReadPartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFReadPartition(data, off, data.length - off);
}
}
public void processSFReadPartitionQuery(byte[] data) {
sendAllQueryReplies();
}
public void processSFReadPartitionQueryList(byte[] data) {
if (data != null && data.length >= 6) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
int qlStart = off + 6;
if (data.length > qlStart) {
byte[] codes = new byte[data.length - qlStart];
System.arraycopy(data, qlStart, codes, 0, codes.length);
sendRequestedQueryReplies(codes);
} else {
sendAllQueryReplies();
}
} else {
sendAllQueryReplies();
}
}
public void processSetReplyMode(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSetReplyMode(data, off, data.length - off);
}
}
public void processCreatePartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processCreatePartition(data, off, data.length - off);
}
}
public void processDestroyPartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processDestroyPartition(data, off, data.length - off);
}
}
public void processActivatePartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processActivatePartition(data, off, data.length - off);
}
}
public void processEraseReset(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processEraseReset(data, off, data.length - off);
} }
} }
@@ -15,16 +15,17 @@ public class QueryReplyBuilder {
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName()); private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
// Canned values from 3279-2 (matching sf.c)
private static final int SW_3279_2 = 0x09; private static final int SW_3279_2 = 0x09;
private static final int SH_3279_2 = 0x0c; private static final int SH_3279_2 = 0x0c;
private static final int Xr_3279_2 = 0x000a02e5;
private static final int Yr_3279_2 = 0x0002006f; // Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060)
private static final int Xr_HOD = 0x00010060;
private static final int Yr_HOD = 0x00010060;
private final ScreenBuffer screen; private final ScreenBuffer screen;
private GraphicsMode graphicsMode = GraphicsMode.BOTH; private GraphicsMode graphicsMode = GraphicsMode.BOTH;
// Base query reply codes (text mode) // Base query reply codes (text mode, matches HOD DS3270.java queryEquiv)
private static final int[] SUPPORTED_QR_BASE = { private static final int[] SUPPORTED_QR_BASE = {
QR_SUMMARY, // 0x80 summary must list itself QR_SUMMARY, // 0x80 summary must list itself
QR_USABLE_AREA, // 0x81 QR_USABLE_AREA, // 0x81
@@ -34,10 +35,11 @@ public class QueryReplyBuilder {
QR_HIGHLIGHTING, // 0x87 QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88 QR_REPLY_MODES, // 0x88
QR_DDM, // 0x95 - Distributed Data Management (file transfer) QR_DDM, // 0x95 - Distributed Data Management (file transfer)
QR_IMP_PART, // 0xa6 QR_AUXDA, // 0x99 - Auxiliary Devices
QR_IMP_PART, // 0xa6 - Implicit Partition Sizes
}; };
// Vector graphics query reply codes matching HOD DS3270.java line 1723 // Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING
private static final int[] SUPPORTED_QR_VECTOR = { private static final int[] SUPPORTED_QR_VECTOR = {
QR_SUMMARY, // 0x80 QR_SUMMARY, // 0x80
QR_USABLE_AREA, // 0x81 QR_USABLE_AREA, // 0x81
@@ -46,17 +48,17 @@ public class QueryReplyBuilder {
QR_COLOR, // 0x86 QR_COLOR, // 0x86
QR_HIGHLIGHTING, // 0x87 QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88 QR_REPLY_MODES, // 0x88
QR_SAVE_RESTORE, // 0x8c QR_OUTLINING, // 0x8c
QR_DDM, // 0x95 QR_DDM, // 0x95
QR_TRANSPARENCY, // 0x99 QR_AUXDA, // 0x99
QR_IMP_PART, // 0xa6 QR_IMP_PART, // 0xa6
QR_RPQ_NAMES, // 0xa8 QR_TRANSPARENCY, // 0xa8
QR_GRAPHICS, // 0xb0 QR_SEGMENT, // 0xb0
QR_GIMAGE, // 0xb1 QR_PROCEDURE, // 0xb1
QR_AUX_DEV, // 0xb2 QR_LINETYPE, // 0xb2
QR_OEM_FMT, // 0xb3 QR_PORT, // 0xb3
QR_GCOLOR, // 0xb4 QR_GRCOLOR, // 0xb4
QR_GSYMBOLS, // 0xb6 QR_GRSYMBOLSET, // 0xb6
}; };
public QueryReplyBuilder(ScreenBuffer screen) { public QueryReplyBuilder(ScreenBuffer screen) {
@@ -77,7 +79,8 @@ public class QueryReplyBuilder {
} }
/** /**
* Build all query replies as a single AID_SF + structured field response. * Build base query replies in response to a generic Read Partition Query (0x02).
* Returns base text/presentation summary structured fields (matches HOD DS3270.java line 1723).
*/ */
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) { public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(512); ByteArrayOutputStream out = new ByteArrayOutputStream(512);
@@ -85,62 +88,90 @@ public class QueryReplyBuilder {
// AID byte for structured field // AID byte for structured field
out.write(AID_SF); out.write(AID_SF);
// Summary // Summary (0x80) - lists all supported capabilities
appendQueryReply(out, QR_SUMMARY, buildSummary()); appendQueryReply(out, QR_SUMMARY, buildSummary());
// Usable Area // Usable Area (0x81)
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize)); appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
// Alpha Partitions // Alpha Partitions (0x84)
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows)); appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
// Character Sets // Character Sets (0x85)
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
// Color // Color (0x86)
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, buildColor());
// Highlighting // Highlighting (0x87)
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
// Reply Modes (0x88) // Reply Modes (0x88)
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
if (graphicsMode.isVectorGraphicsEnabled()) { boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
// Save/Restore (0x8C) if (isDbcs) {
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); // Outlining (0x8C)
appendQueryReply(out, QR_OUTLINING, buildOutlining());
// DBCS Asia (0x91)
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
} }
// Distributed Data Management (0x95) // Distributed Data Management (0x95)
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
if (graphicsMode.isVectorGraphicsEnabled()) { // Auxiliary Devices (0x99)
// Transparency (0x99) appendQueryReply(out, QR_AUXDA, buildAuxDa());
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
}
// Implicit Partition (0xA6) // Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
// Vector Graphics QRs if enabled log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray();
}
/**
* Build complete query replies including vector graphics (when SF_RPQ_ALL 0x80 is requested).
*/
public byte[] buildCompleteQueryReplies(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
out.write(AID_SF);
appendQueryReply(out, QR_SUMMARY, buildSummary());
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
appendQueryReply(out, QR_CHARSETS, buildCharsets());
appendQueryReply(out, QR_COLOR, buildColor());
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
appendQueryReply(out, QR_OUTLINING, buildOutlining());
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
}
appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, buildAuxDa());
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8 appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0 appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1 appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2 appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2
appendOemFmt(out); // 0xB3 appendPort(out); // 0xB3
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4 appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6 appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
} }
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")"); log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray(); return out.toByteArray();
} }
/** /**
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST). * Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field * For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
* matching x3270 sf.c behavior. * matching HOD DS3270.java line 1835.
*/ */
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) { public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
if (requestedCodes == null || requestedCodes.length == 0) { if (requestedCodes == null || requestedCodes.length == 0) {
@@ -174,72 +205,70 @@ public class QueryReplyBuilder {
case QR_REPLY_MODES: case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
break; break;
case QR_SAVE_RESTORE: case QR_OUTLINING: // 0x8C
if (graphicsMode.isVectorGraphicsEnabled()) { appendQueryReply(out, QR_OUTLINING, buildOutlining());
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); break;
case QR_DBCS_ASIA: // 0x91
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_DDM: case QR_DDM: // 0x95
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
break; break;
case QR_TRANSPARENCY: case QR_AUXDA: // 0x99
appendQueryReply(out, QR_AUXDA, buildAuxDa());
break;
case QR_IMP_PART: // 0xA6
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_IMP_PART: case QR_SEGMENT: // 0xB0
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break;
case QR_RPQ_NAMES:
case QR_RPQNAMES:
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, code, buildRpqNames()); appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows));
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_GRAPHICS: case QR_PROCEDURE: // 0xB1
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows));
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_GIMAGE: case QR_LINETYPE: // 0xB2
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); appendQueryReply(out, QR_LINETYPE, buildLineType());
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_AUX_DEV: case QR_PORT: // 0xB3
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); appendPort(out);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_OEM_FMT: case QR_GRCOLOR: // 0xB4
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendOemFmt(out); appendQueryReply(out, QR_GRCOLOR, buildGrColor());
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_GCOLOR: case QR_GRSYMBOLSET: // 0xB6
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GCOLOR, buildGColor()); appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet());
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GSYMBOLS:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -265,34 +294,44 @@ public class QueryReplyBuilder {
out.write(data, 0, data.length); out.write(data, 0, data.length);
} }
private byte[] buildSummary() { public byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE; int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
for (int code : codes) { for (int code : codes) {
out.write(code); out.write(code);
if (isDbcs && code == QR_OUTLINING) {
out.write(QR_DBCS_ASIA); // 0x91
}
} }
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) { public byte[] buildUsableArea() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildUsableArea(maxCols, maxRows, maxCols * maxRows);
}
public byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(19); ByteArrayOutputStream out = new ByteArrayOutputStream(19);
out.write(0x01); // 12/14-bit addressing out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
out.write(0x00); // no special character features out.write(0x00); // no special character features
out.write((maxCols >> 8) & 0xFF); // usable width high out.write((maxCols >> 8) & 0xFF); // usable width high
out.write(maxCols & 0xFF); // usable width low out.write(maxCols & 0xFF); // usable width low
out.write((maxRows >> 8) & 0xFF); // usable height high out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low out.write(maxRows & 0xFF); // usable height low
out.write(0x01); // units (mm) out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
// Xr (4 bytes) - canned from 3279-2 // Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
out.write((Xr_3279_2 >> 24) & 0xFF); out.write((Xr_HOD >> 24) & 0xFF);
out.write((Xr_3279_2 >> 16) & 0xFF); out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_3279_2 >> 8) & 0xFF); out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_3279_2 & 0xFF); out.write(Xr_HOD & 0xFF);
// Yr (4 bytes) - canned from 3279-2 // Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
out.write((Yr_3279_2 >> 24) & 0xFF); out.write((Yr_HOD >> 24) & 0xFF);
out.write((Yr_3279_2 >> 16) & 0xFF); out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_3279_2 >> 8) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF);
out.write(Yr_3279_2 & 0xFF); out.write(Yr_HOD & 0xFF);
int charW = getCharWidth(); int charW = getCharWidth();
int charH = getCharHeight(); int charH = getCharHeight();
out.write(charW); // AW out.write(charW); // AW
@@ -304,6 +343,9 @@ public class QueryReplyBuilder {
} }
public int getCharWidth() { public int getCharWidth() {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12;
}
return SW_3279_2; // 9 return SW_3279_2; // 9
} }
@@ -323,8 +365,14 @@ public class QueryReplyBuilder {
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
private byte[] buildAlphaPartitions(int maxRows) { public byte[] buildAlphaPartitions() {
int bufSize = screen.getMaxCols() * screen.getMaxRows(); int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildAlphaPartitions(maxRows);
}
public byte[] buildAlphaPartitions(int maxRows) {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int bufSize = maxCols * maxRows;
ByteArrayOutputStream out = new ByteArrayOutputStream(4); ByteArrayOutputStream out = new ByteArrayOutputStream(4);
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition) out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high out.write((bufSize >> 8) & 0xFF); // total partition storage high
@@ -333,7 +381,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildCharsets() { public byte[] buildCharsets() {
int charW = getCharWidth(); int charW = getCharWidth();
int charH = getCharHeight(); int charH = getCharHeight();
@@ -344,8 +392,8 @@ public class QueryReplyBuilder {
cpgid = screen.getTranslator().getCpgid(); cpgid = screen.getTranslator().getCpgid();
} }
if (graphicsMode.isProgrammedSymbolsEnabled()) { if (graphicsMode == GraphicsMode.PROGRAMMED_SYMBOLS) {
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID) // Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65); ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02) out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
out.write(0x00); // more flags out.write(0x00); // more flags
@@ -393,7 +441,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildColor() { public byte[] buildColor() {
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total) // Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
return new byte[] { return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4, 0x00, 0x08, 0x00, (byte) 0xF4,
@@ -407,7 +455,7 @@ public class QueryReplyBuilder {
}; };
} }
private byte[] buildHighlighting() { public byte[] buildHighlighting() {
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total) // Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
return new byte[] { return new byte[] {
0x04, 0x00, (byte) 0xF0, 0x04, 0x00, (byte) 0xF0,
@@ -417,11 +465,15 @@ public class QueryReplyBuilder {
}; };
} }
private byte[] buildReplyModes() { public byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
} }
private byte[] buildDdm(int bufferSize) { public byte[] buildDdm() {
return buildDdm(4096);
}
public byte[] buildDdm(int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(8); ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved out.write(0x00); // reserved
out.write(0x00); // reserved out.write(0x00); // reserved
@@ -434,7 +486,13 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildImplicitPartition(int maxCols, int maxRows) { public byte[] buildImplicitPartition() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildImplicitPartition(maxCols, maxRows);
}
public byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22); ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters // Implicit partition sizes, 2 self-defining parameters
@@ -458,7 +516,34 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildGraphics(int maxCols, int maxRows) { public byte[] buildOutlining() {
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000")
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
}
public byte[] buildDbcsAsia() {
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001")
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
}
public byte[] buildAuxDa() {
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000")
return new byte[]{ 0x00, 0x00 };
}
public byte[] buildTransparency() {
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff")
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[]{ return new byte[]{
(byte) 0x80, 0x02, (byte) 0x80, 0x02,
0x00, 0x00, 0x00, 0x00,
@@ -467,7 +552,22 @@ public class QueryReplyBuilder {
}; };
} }
private byte[] buildGImage(int maxCols, int maxRows) { 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[]{ return new byte[]{
0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00,
@@ -478,11 +578,16 @@ public class QueryReplyBuilder {
}; };
} }
public byte[] buildAuxDevice() { public byte[] buildGImage() {
return buildAuxDev(); return buildProcedure();
} }
private byte[] buildAuxDev() { public byte[] buildGImage(int maxCols, int maxRows) {
return buildProcedure(maxCols, maxRows);
}
public byte[] buildLineType() {
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b")
return new byte[]{ return new byte[]{
0x00, 0x09, 0x00, 0x07, 0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
@@ -490,38 +595,46 @@ public class QueryReplyBuilder {
}; };
} }
private byte[] buildSaveRestore() { public byte[] buildAuxDev() {
// HOD DS3270.java line 1768: 6 bytes payload return buildLineType();
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
} }
private byte[] buildTransparency() { public byte[] buildAuxDevice() {
// HOD DS3270.java line 1782: 2 bytes payload return buildLineType();
return new byte[]{ 0x00, 0x00 };
} }
private byte[] buildRpqNames() { public void appendPort(ByteArrayOutputStream out) {
// HOD DS3270.java line 1798: 5 bytes payload // HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total)
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF }; appendQueryReply(out, QR_PORT, new byte[]{
}
private void appendOemFmt(ByteArrayOutputStream out) {
// HOD DS3270.java line 1814: 4 distinct OEM format sub-fields
appendQueryReply(out, QR_OEM_FMT, new byte[]{
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
}); });
appendQueryReply(out, QR_OEM_FMT, new byte[]{ appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
}); });
appendQueryReply(out, QR_OEM_FMT, new byte[]{ appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF 0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
}); });
appendQueryReply(out, QR_OEM_FMT, new byte[]{ appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
}); });
} }
private byte[] buildGColor() { public void appendOemFmt(ByteArrayOutputStream out) {
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); ByteArrayOutputStream out = new ByteArrayOutputStream(110);
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF); out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
@@ -541,11 +654,39 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildGSymbols() { public byte[] buildGraphicColor() {
return buildGrColor();
}
public byte[] buildGColor() {
return buildGrColor();
}
public byte[] buildGrSymbolSet() {
int charW = getCharWidth();
int charH = getCharHeight();
int cgcsgid = 0x02B9;
int cpgid = 0x0025;
if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid();
}
return new byte[]{ return new byte[]{
0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00, 0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00,
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 (byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00
}; };
} }
public byte[] buildGSymbols() {
return buildGrSymbolSet();
}
public byte[] buildSaveRestore() {
return buildOutlining();
}
public byte[] buildRpqNames() {
return buildTransparency();
}
} }
@@ -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 // Search directions
int SEARCH_FORWARD = 1; int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2; int SEARCH_BACKWARD = 2;
int DIR_FORWARD = 1;
int DIR_BACKWARD = 2;
// OIA Input Inhibited Reason Codes // OIA Input Inhibited Reason Codes
int INHIBIT_NOT_INHIBITED = 0; int INHIBIT_NOT_INHIBITED = 0;
@@ -49,4 +51,23 @@ public interface ECLConstants {
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow) int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check) int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due) int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
// Alphanumeric entry types (ECLOIA.getAlphanumericType())
int TYPE_ALPHANUMERIC = 0;
int TYPE_NUMERIC = 1;
int TYPE_DBCS = 2;
int ALPHANUMERIC_NORMAL = 0;
int ALPHANUMERIC_NUMERIC = 1;
int ALPHANUMERIC_DBCS = 2;
// Status condition flags
int STATUS_READY = 0;
int STATUS_X_SYSTEM = 1;
int STATUS_X_NUM = 2;
int STATUS_X_PROT = 3;
int STATUS_X_WAIT = 4;
int STATUS_X_INSERT = 5;
int STATUS_X_COMM = 6;
int STATUS_X_OVERFLOW = 7;
int STATUS_X_OP = 8;
} }
@@ -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; package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -26,67 +28,93 @@ public class ECLField {
/** Buffer address of the field attribute character. */ /** Buffer address of the field attribute character. */
public int getStart() { return startPos; } public int getStart() { return startPos; }
public int GetStart() { return getStart(); }
/** First buffer address of the field data (start + 1). */ /** First buffer address of the field data (start + 1). */
public int getDataStart() { return dataStart; } public int getDataStart() { return dataStart; }
public int GetDataStart() { return getDataStart(); }
/** Last buffer address of the field data inclusive. */ /** Last buffer address of the field data inclusive. */
public int getEnd() { return endPos; } public int getEnd() { return endPos; }
public int GetEnd() { return getEnd(); }
/** Number of data characters in the field. */ /** Number of data characters in the field. */
public int getLength() { return length; } public int getLength() { return length; }
public int GetLength() { return getLength(); }
public int getStartRow() { public int getStartRow() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? startPos / cols : 0; return cols > 0 ? startPos / cols : 0;
} }
public int GetStartRow() { return getStartRow(); }
public int getStartCol() { public int getStartCol() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? startPos % cols : 0; return cols > 0 ? startPos % cols : 0;
} }
public int GetStartCol() { return getStartCol(); }
public int getEndRow() { public int getEndRow() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? endPos / cols : 0; return cols > 0 ? endPos / cols : 0;
} }
public int GetEndRow() { return getEndRow(); }
public int getEndCol() { public int getEndCol() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? endPos % cols : 0; 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() { public boolean isModified() {
return faIsModified(attribute & 0xFF); return faIsModified(getLiveAttribute() & 0xFF);
} }
public boolean IsModified() { return isModified(); }
public boolean isProtected() { public boolean isProtected() {
return faIsProtected(attribute & 0xFF); return faIsProtected(getLiveAttribute() & 0xFF);
} }
public boolean IsProtected() { return isProtected(); }
public boolean isNumeric() { public boolean isNumeric() {
return faIsNumeric(attribute & 0xFF); return faIsNumeric(getLiveAttribute() & 0xFF);
} }
public boolean IsNumeric() { return isNumeric(); }
public boolean isHighIntensity() { public boolean isHighIntensity() {
return faIsHigh(attribute & 0xFF); return faIsHigh(getLiveAttribute() & 0xFF);
} }
public boolean IsHighIntensity() { return isHighIntensity(); }
public boolean isHidden() { public boolean isHidden() {
return faIsZero(attribute & 0xFF); return faIsZero(getLiveAttribute() & 0xFF);
} }
public boolean IsHidden() { return isHidden(); }
public boolean isDisplay() { public boolean isDisplay() {
return !isHidden(); return !isHidden();
} }
public boolean IsDisplay() { return isDisplay(); }
public boolean isPenSelectable() { public boolean isPenSelectable() {
return faIsSelectable(attribute & 0xFF); return faIsSelectable(getLiveAttribute() & 0xFF);
} }
public boolean IsPenSelectable() { return isPenSelectable(); }
public short getAttribute() { 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. * Get the text contents of this field as a String.
@@ -95,6 +123,7 @@ public class ECLField {
if (length <= 0) return ""; if (length <= 0) return "";
return ps.getString(dataStart, length); return ps.getString(dataStart, length);
} }
public String GetText() { return getText(); }
/** /**
* Set the text contents of this field. * Set the text contents of this field.
@@ -103,6 +132,7 @@ public class ECLField {
if (isProtected() || length <= 0) return; if (isProtected() || length <= 0) return;
ps.setText(text, dataStart); ps.setText(text, dataStart);
} }
public void SetText(String text) { setText(text); }
/** /**
* Get selector light pen type. * Get selector light pen type.
@@ -117,6 +147,7 @@ public class ECLField {
} }
return ' '; return ' ';
} }
public char GetSelectorPenType() { return getSelectorPenType(); }
/** /**
* Actuate lightpen selection on this field ('?' -> '>'). * Actuate lightpen selection on this field ('?' -> '>').
@@ -128,6 +159,7 @@ public class ECLField {
setText(">" + (t.length() > 1 ? t.substring(1) : "")); setText(">" + (t.length() > 1 ? t.substring(1) : ""));
} }
} }
public void SelectField() { selectField(); }
/** /**
* Deselect lightpen selection on this field ('>' -> '?'). * Deselect lightpen selection on this field ('>' -> '?').
@@ -139,6 +171,100 @@ public class ECLField {
setText("?" + (t.length() > 1 ? t.substring(1) : "")); 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 @Override
public String toString() { public String toString() {
@@ -70,27 +70,38 @@ public class ECLFieldList {
} }
} }
public void Refresh() { refresh(); }
public synchronized int getFieldCount() { public synchronized int getFieldCount() {
return fields.size(); return fields.size();
} }
public int GetFieldCount() { return getFieldCount(); }
public synchronized List<ECLField> getFields() { public synchronized List<ECLField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields)); return Collections.unmodifiableList(new ArrayList<>(fields));
} }
public List<ECLField> GetFields() { return getFields(); }
public synchronized ECLField getFirstField() { public synchronized ECLField getFirstField() {
if (fields.isEmpty()) return null; if (fields.isEmpty()) return null;
return fields.get(0); return fields.get(0);
} }
public ECLField GetFirstField() { return getFirstField(); }
public synchronized ECLField getNextField(ECLField prev) { public synchronized ECLField getNextField(ECLField prev) {
if (prev == null || fields.isEmpty()) return getFirstField(); if (prev == null || fields.isEmpty()) return getFirstField();
int idx = fields.indexOf(prev); for (int i = 0; i < fields.size(); i++) {
if (idx >= 0 && idx + 1 < fields.size()) { ECLField f = fields.get(i);
return fields.get(idx + 1); if (f.equals(prev) || f.getStart() == prev.getStart()) {
if (i + 1 < fields.size()) {
return fields.get(i + 1);
}
return null;
}
} }
return null; return null;
} }
public ECLField GetNextField(ECLField prev) { return getNextField(prev); }
/** /**
* Find the field that contains the specified buffer position. * Find the field that contains the specified buffer position.
@@ -113,6 +124,7 @@ public class ECLFieldList {
} }
return null; return null;
} }
public ECLField FindField(int pos) { return findField(pos); }
/** /**
* Find the field at the specified 0-indexed row and column. * Find the field at the specified 0-indexed row and column.
@@ -122,6 +134,62 @@ public class ECLFieldList {
int cols = screen.getCols(); int cols = screen.getCols();
return findField(row * cols + col); 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. * Find field containing the given text string.
@@ -147,6 +215,14 @@ public class ECLFieldList {
return f; return f;
} }
} }
// Wrap around search to beginning of field list
for (int i = 0; i < startIdx; i++) {
ECLField f = fields.get(i);
if (f.getText().contains(text)) {
return f;
}
}
return null; return null;
} }
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.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.telnet.TelnetFSM; import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
@@ -18,6 +19,7 @@ public class ECLOIA implements ECLConstants {
private final InputProcessor inputProcessor; private final InputProcessor inputProcessor;
private final TelnetFSM fsm; private final TelnetFSM fsm;
private final List<ECLOIANotify> listeners = new ArrayList<>(); private final List<ECLOIANotify> listeners = new ArrayList<>();
private final List<ECLOIAListener> oiaListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
public interface ECLOIANotify { public interface ECLOIANotify {
void onOIAChanged(ECLOIA oia); void onOIAChanged(ECLOIA oia);
@@ -43,41 +45,233 @@ public class ECLOIA implements ECLConstants {
listeners.remove(listener); 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() { private synchronized void notifyOIAChanged() {
for (ECLOIANotify l : listeners) { for (ECLOIANotify l : listeners) {
try { try {
l.onOIAChanged(this); l.onOIAChanged(this);
} catch (Exception ignored) {} } 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() { public boolean isInsertMode() {
return inputProcessor != null && inputProcessor.isInsertMode(); return inputProcessor != null && inputProcessor.isInsertMode();
} }
public boolean IsInsertMode() {
return isInsertMode();
}
public boolean isNumeric() { public boolean isNumeric() {
if (screen == null || !screen.isFormatted()) return false; if (screen == null || !screen.isFormatted()) return false;
byte fa = screen.getFieldAttributeAt(screen.getCursorAddress()); byte fa = screen.getFieldAttributeAt(screen.getCursorAddress());
return faIsNumeric(fa & 0xFF); return faIsNumeric(fa & 0xFF);
} }
public boolean IsNumeric() {
return isNumeric();
}
public boolean isAlphanumeric() { public boolean isAlphanumeric() {
return !isNumeric(); return !isNumeric();
} }
public boolean IsAlphanumeric() {
return isAlphanumeric();
}
public boolean isDBCS() {
return getAlphanumericType() == TYPE_DBCS;
}
public boolean IsDBCS() {
return isDBCS();
}
public boolean isMessageWaiting() { public boolean isMessageWaiting() {
return false; return false;
} }
public boolean IsMessageWaiting() {
return isMessageWaiting();
}
public boolean isCommError() { public boolean isCommError() {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected(); 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. * Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants. * Returns one of INHIBIT_* constants from ECLConstants.
*/ */
public int getInputInhibited() { public int getInputInhibited() {
if (inhibitOverride >= 0) {
return inhibitOverride;
}
if (isCommError()) { if (isCommError()) {
return INHIBIT_COMM_CHECK; return INHIBIT_COMM_CHECK;
} }
@@ -87,6 +281,18 @@ public class ECLOIA implements ECLConstants {
return INHIBIT_NOT_INHIBITED; 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. * Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
* @return true if keyboard unlocked, false if timeout occurred. * @return true if keyboard unlocked, false if timeout occurred.
@@ -114,6 +320,14 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs); return waitForInput(timeoutMs);
} }
public boolean WaitForSystemAvailable(long timeoutMs) {
return waitForSysAvailable(timeoutMs);
}
public boolean WaitForInput(long timeoutMs) {
return waitForInput(timeoutMs);
}
/** /**
* Block until application is available. * Block until application is available.
*/ */
@@ -121,6 +335,10 @@ public class ECLOIA implements ECLConstants {
return waitForInput(timeoutMs); return waitForInput(timeoutMs);
} }
public boolean WaitForAppAvailable(long timeoutMs) {
return waitForAppAvailable(timeoutMs);
}
/** /**
* Block until any OIA transition occurs. * Block until any OIA transition occurs.
*/ */
@@ -140,4 +358,8 @@ public class ECLOIA implements ECLConstants {
} }
return getInputInhibited() != initialInhibit; 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; return fieldList;
} }
private boolean nvtMode = false;
public boolean isNVTmode() {
if (inputProcessor != null && inputProcessor.isNvtMode()) {
return true;
}
return nvtMode;
}
public void setNVTmode(boolean nvt) {
this.nvtMode = nvt;
}
public int getSize() { return screen.getRows() * screen.getCols(); } public int getSize() { return screen.getRows() * screen.getCols(); }
public int getRows() { return screen.getRows(); } public int getRows() { return screen.getRows(); }
public int getCols() { return screen.getCols(); } public int getCols() { return screen.getCols(); }
@@ -139,8 +152,8 @@ public class ECLPS implements ECLConstants {
} }
/** /**
* Search for a string in the presentation space. * Search for a string in the presentation space (0-based indexing).
* Returns 1-based or 0-based position, or -1 if not found. * Returns 0-based position, or -1 if not found.
*/ */
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) { public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
if (target == null || target.isEmpty() || screen == null) return -1; if (target == null || target.isEmpty() || screen == null) return -1;
@@ -161,7 +174,6 @@ public class ECLPS implements ECLConstants {
int targetLen = target.length(); int targetLen = target.length();
if (dir == SEARCH_FORWARD) { if (dir == SEARCH_FORWARD) {
// Forward search with wrapping
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
int pos = (startPos + i) % size; int pos = (startPos + i) % size;
if (matchesAt(screenText, target, pos, size)) { if (matchesAt(screenText, target, pos, size)) {
@@ -169,7 +181,6 @@ public class ECLPS implements ECLConstants {
} }
} }
} else { } else {
// Backward search with wrapping
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
int pos = (startPos - i + size) % size; int pos = (startPos - i + size) % size;
if (matchesAt(screenText, target, pos, size)) { if (matchesAt(screenText, target, pos, size)) {
@@ -180,6 +191,147 @@ public class ECLPS implements ECLConstants {
return -1; return -1;
} }
public int searchString(String target) {
return searchString(target, 0, 0, SEARCH_FORWARD, false);
}
public int searchString(String target, int startRow, int startCol) {
return searchString(target, startRow, startCol, SEARCH_FORWARD, false);
}
// ========== IBM HoD SearchPS / SearchPSExt (1-based API) ==========
public int SearchPS(String text) {
return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true);
}
public int searchPS(String text) {
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) { private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length(); int len = target.length();
for (int j = 0; j < len; j++) { for (int j = 0; j < len; j++) {
@@ -191,6 +343,84 @@ public class ECLPS implements ECLConstants {
return true; return true;
} }
// ========== Rectangular Block Copy & Paste ==========
/**
* Copy a rectangular text region from (sRow, sCol) to (eRow, eCol) inclusive.
*/
public synchronized String copyString(int sRow, int sCol, int eRow, int eCol) {
if (screen == null) return "";
int rows = screen.getRows();
int cols = screen.getCols();
if (rows <= 0 || cols <= 0) return "";
int minR = Math.max(0, Math.min(sRow, eRow));
int maxR = Math.min(rows - 1, Math.max(sRow, eRow));
int minC = Math.max(0, Math.min(sCol, eCol));
int maxC = Math.min(cols - 1, Math.max(sCol, eCol));
int sliceWidth = maxC - minC + 1;
StringBuilder sb = new StringBuilder();
for (int r = minR; r <= maxR; r++) {
char[] rowBuf = new char[sliceWidth];
getPlane(PLANE_TEXT, rowBuf, r * cols + minC, sliceWidth);
sb.append(rowBuf);
if (r < maxR) {
sb.append("\n");
}
}
return sb.toString();
}
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. * Paste text with line wrapping across unprotected fields.
*/ */
@@ -211,21 +441,186 @@ public class ECLPS implements ECLConstants {
int curPos = screen.getCursorAddress(); int curPos = screen.getCursorAddress();
int curCol = curPos % cols; int curCol = curPos % cols;
if (endCol > 0 && curCol >= endCol) { if (endCol > 0 && curCol >= endCol) {
// Advance to next row
int nextRow = (curPos / cols + 1) % rows; int nextRow = (curPos / cols + 1) % rows;
setCursorPos(nextRow * cols); setCursorPos(nextRow * cols);
} }
inputProcessor.typeCharacter(line.charAt(i)); if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(i));
}
charsPasted++; charsPasted++;
} }
if (l < lines.length - 1) { if (l < lines.length - 1 && inputProcessor != null) {
// Newline key between lines
inputProcessor.newline(); inputProcessor.newline();
} }
} }
return charsPasted; 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. * Send standard IBM ECL mnemonic keystrokes.
*/ */
@@ -234,4 +629,183 @@ public class ECLPS implements ECLConstants {
inputProcessor.sendKeys(keys); 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.FTConstants.FTState;
import haus.nightmare.lib3270j.ft.FTCut; import haus.nightmare.lib3270j.ft.FTCut;
import haus.nightmare.lib3270j.ft.FTDft; import haus.nightmare.lib3270j.ft.FTDft;
import haus.nightmare.lib3270j.ft.CMSPrintXfer;
import haus.nightmare.lib3270j.ft.dir.*; import haus.nightmare.lib3270j.ft.dir.*;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; 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. * 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) { public void getFile(String hostFile, String localFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener); if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) { if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -131,9 +140,27 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
ReceiveFile(localFile, hostFile, options); 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. * 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) { public void putFile(String localFile, String hostFile, String options, int mode, String codePage, ECLXferListener listener) {
if (listener != null) addXferListener(listener); if (listener != null) addXferListener(listener);
if (codePage != null && !codePage.trim().isEmpty() && translator != null) { if (codePage != null && !codePage.trim().isEmpty() && translator != null) {
@@ -142,6 +169,16 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
SendFile(localFile, hostFile, options); 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. * Cancel an active transfer.
* @return 0 on success. * @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) { public CMSDirectoryEntry createNewCmsDirectoryEntry(String fn, String ft, String fm) {
return new CMSDirectoryEntry(fn, ft, fm); return new CMSDirectoryEntry(fn, ft, fm);
} }
@@ -251,6 +363,15 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
return new TSODirectoryEntry(dsname); return new TSODirectoryEntry(dsname);
} }
private CMSPrintXfer cmsPrintXfer;
public CMSPrintXfer getCMSPrintXfer() {
if (cmsPrintXfer == null) {
cmsPrintXfer = new CMSPrintXfer(this, translator);
}
return cmsPrintXfer;
}
// ========== BIDI File Helpers ========== // ========== BIDI File Helpers ==========
public void doBIDIsaveLocalFile(File file, boolean rtl) throws IOException { 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) { private void parseOptionsIntoConfig(FTConfig config, String options) {
if (options == null || options.trim().isEmpty()) return; if (options == null || options.trim().isEmpty()) return;
String upper = options.toUpperCase(); config.parseOptions(options);
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);
} }
/** /**
@@ -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) { 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 ========== // ========== Validation ==========
@@ -71,6 +71,9 @@ public class FTDft {
public void setMTUSize(int size) { public void setMTUSize(int size) {
this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF, this.customMtuSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, size)); Math.min(FTConstants.DFT_MAX_BUF, size));
if (listener != null && listener.getConfig() != null) {
listener.getConfig().setDftBufferSize(this.customMtuSize);
}
} }
public int getMTUSize() { public int getMTUSize() {
@@ -414,9 +417,9 @@ public class FTDft {
log.info("DFT host message: " + msg); log.info("DFT host message: " + msg);
String msgUpper = msg.toUpperCase(); 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); 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); listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
} else { } else {
// Informational message (default success) // Informational message (default success)
@@ -480,7 +483,7 @@ public class FTDft {
return; return;
} }
int bufferSize = config.getDftBufferSize(); int bufferSize = getMTUSize();
int numbytes = bufferSize - 27; int numbytes = bufferSize - 27;
byte[] readBuf = new byte[numbytes]; byte[] readBuf = new byte[numbytes];
int totalRead = 0; int totalRead = 0;
@@ -569,6 +572,71 @@ public class FTDft {
listener.onBytesTransferred(bytesTransferred); 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. * Read a byte from local file for upload, handling ASCII conversion and remapping.
* Matching x3270 dft_ascii_read logic. * Matching x3270 dft_ascii_read logic.
@@ -19,21 +19,36 @@ public class FillArea {
public static class Edge { public static class Edge {
public final double x1, y1; public final double x1, y1;
public final double x2, y2; public final double x2, y2;
public final int direction; // +1 if y1 < y2 (upward), -1 if y1 > y2 (downward)
public Edge(double x1, double y1, double x2, double y2) { public Edge(double x1, double y1, double x2, double y2) {
this.x1 = x1; this.x1 = x1;
this.y1 = y1; this.y1 = y1;
this.x2 = x2; this.x2 = x2;
this.y2 = y2; this.y2 = y2;
this.direction = (y2 > y1) ? 1 : -1;
} }
} }
private int fillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private final List<Edge> edges = new ArrayList<>(); private final List<Edge> edges = new ArrayList<>();
private final List<double[]> subpathsX = new ArrayList<>(); private final List<double[]> subpathsX = new ArrayList<>();
private final List<double[]> subpathsY = new ArrayList<>(); private final List<double[]> subpathsY = new ArrayList<>();
public FillArea() {} public FillArea() {}
public FillArea(int fillRule) {
this.fillRule = fillRule;
}
public synchronized void setFillRule(int fillRule) {
this.fillRule = fillRule;
}
public synchronized int getFillRule() {
return fillRule;
}
/** /**
* Adds a single directed edge to the edge table. * Adds a single directed edge to the edge table.
*/ */
@@ -104,12 +119,47 @@ public class FillArea {
subpathsY.clear(); subpathsY.clear();
} }
/**
* Rasterizes and fills a direct polygon on the target GraphicsPlane.
*/
public synchronized void fill(GraphicsPlane plane, int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
clear();
addPolygon(px, py, numPoints);
fill(plane, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, null);
}
/** /**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane. * Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
*/ */
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern, public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, ProgramSymbolManager psm) { int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
fill(plane, fillColorArgb, patternSet, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, this.fillRule, psm);
}
private static class NodeIntersection implements Comparable<NodeIntersection> {
final double x;
final int dir;
NodeIntersection(double x, int dir) {
this.x = x;
this.dir = dir;
}
@Override
public int compareTo(NodeIntersection other) {
return Double.compare(this.x, other.x);
}
}
/**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, int fillRule, ProgramSymbolManager psm) {
if (plane == null) return; if (plane == null) return;
if (edges.isEmpty() && subpathsX.isEmpty()) return; if (edges.isEmpty() && subpathsX.isEmpty()) return;
@@ -140,7 +190,7 @@ public class FillArea {
int iMinY = Math.max(0, (int) Math.floor(minY)); int iMinY = Math.max(0, (int) Math.floor(minY));
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY)); int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
List<Double> nodeX = new ArrayList<>(); List<NodeIntersection> intersections = new ArrayList<>();
byte[] patRows = null; byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null; ProgramSymbolSet.SymbolSlot psSlot = null;
@@ -156,45 +206,84 @@ public class FillArea {
} }
for (int y = iMinY; y <= iMaxY; y++) { for (int y = iMinY; y <= iMaxY; y++) {
nodeX.clear(); intersections.clear();
double scanY = y + 0.5; double scanY = y + 0.5;
for (Edge e : edges) { for (Edge e : edges) {
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) { if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1); double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
nodeX.add(x); intersections.add(new NodeIntersection(x, e.direction));
} }
} }
Collections.sort(nodeX); Collections.sort(intersections);
for (int i = 0; i < nodeX.size(); i += 2) { if (fillRule == GocaConstants.FILL_RULE_WINDING) {
if (i + 1 >= nodeX.size()) break; // Non-Zero Winding Rule: evaluate winding count
int leftX = Math.max(0, (int) Math.round(nodeX.get(i))); int winding = 0;
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1))); for (int i = 0; i < intersections.size() - 1; i++) {
winding += intersections.get(i).dir;
if (winding != 0) {
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) { for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) { if (psSlot != null) {
int psW = psSlot.getWidth(); int psW = psSlot.getWidth();
int psH = psSlot.getHeight(); int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0; int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0; int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData(); byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
plane.setPixel(x, y, fill); plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg); plane.setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
}
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { }
plane.setPixel(x, y, fill); }
} else if (patRows != null) { } else {
int b = patRows[y & 7] & 0xFF; // Even-Odd / Alternate Rule
if (((b >> (7 - (x & 7))) & 1) != 0) { for (int i = 0; i < intersections.size(); i += 2) {
if (i + 1 >= intersections.size()) break;
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill); plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (patRows != null) {
plane.setPixel(x, y, bg); int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
} }
} }
} }
@@ -50,6 +50,8 @@ public final class GocaConstants {
public static final int G_GSMS = 0x1B; // Set Marker Size public static final int G_GSMS = 0x1B; // Set Marker Size
public static final int G_GSCP = 0x21; // Set Current Position public static final int G_GSCP = 0x21; // Set Current Position
public static final int G_GSAP = 0x22; // Arc Parameters public static final int G_GSAP = 0x22; // Arc Parameters
public static final int G_GSC = 0x22; // Segment Characteristics
public static final int G_GSVW_DEF = 0x23; // Set Viewing Window Definition
public static final int G_GSECOL = 0x26; // Set Extended Color public static final int G_GSECOL = 0x26; // Set Extended Color
public static final int G_GSVW = 0x27; // Set Viewing Window public static final int G_GSVW = 0x27; // Set Viewing Window
public static final int G_GSPT = 0x28; // Set Pattern Symbol public static final int G_GSPT = 0x28; // Set Pattern Symbol
@@ -60,10 +62,9 @@ public final class GocaConstants {
public static final int G_GSCR = 0x35; // Set Character Shear public static final int G_GSCR = 0x35; // Set Character Shear
public static final int G_GSMCEL = 0x37; // Set Marker Cell public static final int G_GSMCEL = 0x37; // Set Marker Cell
public static final int G_GSCS = 0x38; // Set Character Set public static final int G_GSCS = 0x38; // Set Character Set
public static final int G_GSMP = 0x39; // Set Marker Precision public static final int G_GSCC = 0x39; // Set Character Precision
public static final int G_GSETAG = 0x39; // Set Pick Identifier / Tag
public static final int G_GSCD = 0x3A; // Set Character Direction public static final int G_GSCD = 0x3A; // Set Character Direction
public static final int G_GSCC = 0x3B; // Set Character Precision public static final int G_GSMP = 0x3B; // Set Marker Precision
public static final int G_GSMS_SET = 0x3C; // Set Marker Set public static final int G_GSMS_SET = 0x3C; // Set Marker Set
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
public static final int G_GPOP = 0x3F; // Pop Attribute public static final int G_GPOP = 0x3F; // Pop Attribute
@@ -158,6 +159,19 @@ public final class GocaConstants {
public static final int MIX_XOR = 4; public static final int MIX_XOR = 4;
public static final int MIX_UNDER = 5; public static final int MIX_UNDER = 5;
// Fill Rules (GBAR 0x68 flags)
public static final int FILL_RULE_EVEN_ODD = 0;
public static final int FILL_RULE_WINDING = 1;
// Image Formats & Compression (GBIMG 0xD1)
public static final int IMG_UNCOMPRESSED = 0;
public static final int IMG_RLE = 1;
public static final int IMG_MMR = 2;
public static final int BPP_1 = 1;
public static final int BPP_2 = 2;
public static final int BPP_4 = 4;
public static final int BPP_8 = 8;
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB) // Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
public static final int[] GOCA_COLORS = new int[] { public static final int[] GOCA_COLORS = new int[] {
0xFF00FF00, // 0: Default (Green) 0xFF00FF00, // 0: Default (Green)
@@ -27,11 +27,14 @@ public class GocaDecoder {
private int markerType = GocaConstants.MK_PLUS; private int markerType = GocaConstants.MK_PLUS;
private int markerSize = 5; private int markerSize = 5;
private int markerColor = GocaConstants.GOCA_COLORS[0]; private int markerColor = GocaConstants.GOCA_COLORS[0];
private int markerPrecision = 0;
private int pattern = GocaConstants.PT_SOLID; private int pattern = GocaConstants.PT_SOLID;
private int patternSet = 0; private int patternSet = 0;
private int fillColor = GocaConstants.GOCA_COLORS[0]; private int fillColor = GocaConstants.GOCA_COLORS[0];
private int charDir = GocaConstants.CD_LR; private int charDir = GocaConstants.CD_LR;
private double charAngle = 0.0; private double charAngle = 0.0;
private double charShear = 0.0;
private double fractionalLineWidth = 1.0;
private int charWidth = 9; private int charWidth = 9;
private int charHeight = 16; private int charHeight = 16;
private int charSet = 0; private int charSet = 0;
@@ -41,11 +44,16 @@ public class GocaDecoder {
private int arcParamR = 0; private int arcParamR = 0;
private int arcParamS = 1; private int arcParamS = 1;
private boolean segChained = false;
private boolean segDynamic = false;
private boolean segVisible = true;
private ProgramSymbolManager programSymbolManager; private ProgramSymbolManager programSymbolManager;
// Area accumulation // Area accumulation
private boolean inArea = false; private boolean inArea = false;
private boolean areaDrawBoundary = true; private boolean areaDrawBoundary = true;
private int areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private boolean areaFill = true; private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>(); private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>(); private final List<Integer> areaPointsY = new ArrayList<>();
@@ -58,13 +66,12 @@ public class GocaDecoder {
private int imgY = 0; private int imgY = 0;
private int imgWidth = 0; private int imgWidth = 0;
private int imgHeight = 0; private int imgHeight = 0;
private int imgBitDepth = GocaConstants.BPP_1;
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
private final List<Byte> imgBuffer = new ArrayList<>(); private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A) // Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
private final java.util.Map<Integer, byte[]> segmentStore = new java.util.HashMap<>(); private final java.util.Map<Integer, byte[]> segmentStore = new java.util.HashMap<>();
private final java.util.Map<Integer, Integer> segmentChainMap = new java.util.HashMap<>();
private final java.util.List<Integer> segmentOrderList = new java.util.ArrayList<>();
private final java.util.Set<Integer> chainedTargets = new java.util.HashSet<>();
private int callDepth = 0; private int callDepth = 0;
/** /**
@@ -148,6 +155,8 @@ public class GocaDecoder {
return graphicCursorY; return graphicCursorY;
} }
public static final int GDDM_CURSOR_OFFSET_Y = 0;
public synchronized void setGraphicCursorPosition(int x, int y) { public synchronized void setGraphicCursorPosition(int x, int y) {
this.graphicCursorX = x; this.graphicCursorX = x;
this.graphicCursorY = y; this.graphicCursorY = y;
@@ -198,6 +207,89 @@ public class GocaDecoder {
} }
} }
public double getCharAngle() {
return charAngle;
}
public synchronized void setCharAngle(double angle) {
this.charAngle = angle;
}
public double getCharShear() {
return charShear;
}
public synchronized void setCharShear(double shear) {
this.charShear = shear;
}
public double getFractionalLineWidth() {
return fractionalLineWidth;
}
public synchronized void setFractionalLineWidth(double flw) {
this.fractionalLineWidth = Math.max(0.1, flw);
if (plane != null) {
plane.setFractionalLineWidth(this.fractionalLineWidth);
}
}
public boolean isSegChained() {
return segChained;
}
public boolean isSegDynamic() {
return segDynamic;
}
public boolean isSegVisible() {
return segVisible;
}
/**
* Processes GOCA order 0x23 / 0x27 Viewing Window clipping viewport.
*/
public synchronized void processViewingWindow(byte[] data) {
if (data == null || data.length < 8) return;
int xMin = readCoord(data, 0);
int yMin = readCoord(data, 2);
int xMax = readCoord(data, 4);
int yMax = readCoord(data, 6);
logger.info(String.format("GOCA processViewingWindow: [%d..%d, %d..%d]", xMin, xMax, yMin, yMax));
if (plane != null) {
plane.setViewingWindow(xMin, yMin, xMax, yMax);
}
}
/**
* Processes GOCA order 0x22 Segment Characteristics (chained/non-chained, dynamic, visible).
*/
public synchronized void processSegmentCharacteristics(byte[] data) {
if (data == null || data.length < 1) return;
int flags = data[0] & 0xFF;
this.segChained = (flags & 0x80) != 0;
this.segDynamic = (flags & 0x40) != 0;
this.segVisible = (flags & 0x20) == 0;
logger.info(String.format("GOCA processSegmentCharacteristics: flags=0x%02x (chained=%b, dynamic=%b, visible=%b)",
flags, segChained, segDynamic, segVisible));
}
/**
* Processes GOCA order 0x11 Fractional Line Width calculation.
*/
public synchronized void processFractionalLineWidth(byte[] data) {
if (data == null || data.length < 1) return;
int intPart = data[0] & 0xFF;
int fracPart = (data.length > 1) ? (data[1] & 0xFF) : 0;
double flw = intPart + (fracPart / 256.0);
if (flw <= 0.0) flw = 1.0;
this.fractionalLineWidth = flw;
if (plane != null) {
plane.setFractionalLineWidth(flw);
}
logger.info("GOCA processFractionalLineWidth: flw=" + flw);
}
public synchronized void resetDefaults() { public synchronized void resetDefaults() {
curX = 0; curX = 0;
curY = 0; curY = 0;
@@ -205,9 +297,6 @@ public class GocaDecoder {
graphicCursorX = 0; graphicCursorX = 0;
graphicCursorY = 0; graphicCursorY = 0;
segmentStore.clear(); segmentStore.clear();
segmentChainMap.clear();
segmentOrderList.clear();
chainedTargets.clear();
segmentBoundsMap.clear(); segmentBoundsMap.clear();
activeSegmentsInOrder.clear(); activeSegmentsInOrder.clear();
currentSegId = 0; currentSegId = 0;
@@ -220,22 +309,31 @@ public class GocaDecoder {
bgColor = GocaConstants.GOCA_COLORS[8]; // Black bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID; lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL; lineWidth = GocaConstants.LW_NORMAL;
fractionalLineWidth = 1.0;
if (plane != null) {
plane.setFractionalLineWidth(1.0);
}
markerType = GocaConstants.MK_PLUS; markerType = GocaConstants.MK_PLUS;
markerSize = 5; markerSize = 5;
markerColor = curColor; markerColor = curColor;
markerPrecision = 0;
pattern = GocaConstants.PT_SOLID; pattern = GocaConstants.PT_SOLID;
patternSet = 0; patternSet = 0;
fillColor = curColor; fillColor = curColor;
charDir = GocaConstants.CD_LR; charDir = GocaConstants.CD_LR;
charAngle = 0.0; charAngle = 0.0;
charShear = 0.0;
charSet = 0; charSet = 0;
charPrecision = GocaConstants.CP_STRING; charPrecision = GocaConstants.CP_STRING;
inArea = false; inArea = false;
areaDrawBoundary = true; areaDrawBoundary = true;
areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
areaFill = true; areaFill = true;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
inImage = false; inImage = false;
imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
imgBuffer.clear(); imgBuffer.clear();
} }
@@ -272,6 +370,10 @@ public class GocaDecoder {
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
} }
// Fractional Line Width (0x11): 2-byte operand [int][frac] or 1-byte operand [int]
if (order == GocaConstants.G_GSFLW) {
return (idx + 2 < end) ? 3 : 2;
}
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.) // All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
if (order < 0x20) { if (order < 0x20) {
return 2; return 2;
@@ -279,7 +381,7 @@ public class GocaDecoder {
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1) // Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1)
if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT || if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT ||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMP ||
order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) { order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2; return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2;
} }
@@ -290,61 +392,6 @@ public class GocaDecoder {
return (data[idx + 1] & 0xFF) + 2; return (data[idx + 1] & 0xFF) + 2;
} }
private void indexSegments(byte[] data, int offset, int length) {
int idx = offset;
int end = offset + length;
while (idx < end) {
int order = data[idx] & 0xFF;
if (order == GocaConstants.G_BEGSEGM) {
int segStart = idx;
int segLen = getOrderLength(data, idx, end);
if (segLen <= 0 || idx + 5 >= end) {
break;
}
int segId = ((data[idx + 2] & 0xFF) << 24) |
((data[idx + 3] & 0xFF) << 16) |
((data[idx + 4] & 0xFF) << 8) |
(data[idx + 5] & 0xFF);
int nextId = 0;
if (segLen >= 14 && (data[idx + 1] & 0xFF) >= 12) {
nextId = ((data[idx + 10] & 0xFF) << 24) |
((data[idx + 11] & 0xFF) << 16) |
((data[idx + 12] & 0xFF) << 8) |
(data[idx + 13] & 0xFF);
}
int searchIdx = idx + segLen;
while (searchIdx < end) {
int o = data[searchIdx] & 0xFF;
int oLen = getOrderLength(data, searchIdx, end);
if (oLen <= 0) break;
if (o == GocaConstants.G_ENDSEGM) {
searchIdx += oLen;
break;
}
searchIdx += oLen;
}
int fullSegLen = searchIdx - segStart;
if (fullSegLen > 0 && segStart + fullSegLen <= end) {
byte[] segBytes = new byte[fullSegLen];
System.arraycopy(data, segStart, segBytes, 0, fullSegLen);
segmentStore.put(segId, segBytes);
segmentOrderList.add(segId);
if (nextId != 0) {
segmentChainMap.put(segId, nextId);
chainedTargets.add(nextId);
}
}
idx = searchIdx;
} else {
int oLen = getOrderLength(data, idx, end);
if (oLen <= 0) break;
idx += oLen;
}
}
}
/** /**
* Decodes a stream of GOCA drawing orders (matching IBM Host On-Demand HODDecoder.decodeGOCA). * Decodes a stream of GOCA drawing orders (matching IBM Host On-Demand HODDecoder.decodeGOCA).
*/ */
@@ -382,7 +429,7 @@ public class GocaDecoder {
} }
/** /**
* Executes a stored procedure segment by segment ID, traversing chained next segments. * Executes a stored procedure segment by segment ID.
*/ */
public synchronized void procedureSegment(int segId) { public synchronized void procedureSegment(int segId) {
if (segId == 0) return; if (segId == 0) return;
@@ -397,12 +444,6 @@ public class GocaDecoder {
decodeStreamDirect(segData, 0, segData.length); decodeStreamDirect(segData, 0, segData.length);
callDepth--; callDepth--;
currentSegId = savedSeg; currentSegId = savedSeg;
// Execute chained segments
Integer nextId = segmentChainMap.get(segId);
if (nextId != null && nextId != 0 && callDepth < 16) {
procedureSegment(nextId);
}
} else { } else {
logger.warning("procedureSegment: Segment not found in store: " + segId); logger.warning("procedureSegment: Segment not found in store: " + segId);
} }
@@ -440,10 +481,6 @@ public class GocaDecoder {
end = offset + length; end = offset + length;
} }
if (callDepth == 0) {
indexSegments(inputData, idx, end - idx);
}
decodeStreamDirect(inputData, idx, end - idx); decodeStreamDirect(inputData, idx, end - idx);
} }
@@ -491,6 +528,27 @@ public class GocaDecoder {
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new); SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
activeSegmentsInOrder.remove(sb); activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb); activeSegmentsInOrder.add(sb);
if (segId != 0 && callDepth == 0) {
int segStart = idx;
int searchIdx = idx + orderLen;
while (searchIdx < end) {
int o = inputData[searchIdx] & 0xFF;
int oLen = getOrderLength(inputData, searchIdx, end);
if (oLen <= 0) break;
if (o == GocaConstants.G_ENDSEGM) {
searchIdx += oLen;
break;
}
searchIdx += oLen;
}
int fullSegLen = searchIdx - segStart;
if (fullSegLen > 0 && segStart + fullSegLen <= end) {
byte[] segBytes = new byte[fullSegLen];
System.arraycopy(inputData, segStart, segBytes, 0, fullSegLen);
segmentStore.put(segId, segBytes);
}
}
} }
if (idx + 6 < end) flag0 = inputData[idx + 6] & 0xFF; if (idx + 6 < end) flag0 = inputData[idx + 6] & 0xFF;
if (idx + 7 < end) flag1 = inputData[idx + 7] & 0xFF; if (idx + 7 < end) flag1 = inputData[idx + 7] & 0xFF;
@@ -503,7 +561,6 @@ public class GocaDecoder {
} }
case GocaConstants.G_ENDSEGM: { // End Segment (0x71) case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
logger.info(String.format("GOCA ENDSEGM: segId=%d", currentSegId)); logger.info(String.format("GOCA ENDSEGM: segId=%d", currentSegId));
int finishedSegId = currentSegId;
if (currentSegId != 0) { if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId); SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) { if (sb != null) {
@@ -515,36 +572,9 @@ public class GocaDecoder {
} }
currentSegId = 0; currentSegId = 0;
idx += orderLen; idx += orderLen;
if (finishedSegId != 0 && callDepth < 16) {
Integer nextId = segmentChainMap.get(finishedSegId);
if (nextId != null && nextId != 0) {
byte[] nextSeg = segmentStore.get(nextId);
if (nextSeg != null) {
logger.info("Executing chained segment nextId=" + nextId);
currentSegId = nextId;
SegmentBounds targetSb = segmentBoundsMap.computeIfAbsent(nextId, SegmentBounds::new);
activeSegmentsInOrder.remove(targetSb);
activeSegmentsInOrder.add(targetSb);
callDepth++;
decodeStreamDirect(nextSeg, 0, nextSeg.length);
callDepth--;
currentSegId = 0;
}
}
}
break;
}
case GocaConstants.G_GSETAG: { // Set Pick Identifier / Tag (0x39)
if (currentSegId != 0 && payloadLen >= 2 && idx + 3 < end) {
int tag = ((inputData[idx + 2] & 0xFF) << 8) | (inputData[idx + 3] & 0xFF);
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) {
sb.tag = tag;
}
}
idx += orderLen;
break; break;
} }
case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E) case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E)
idx += orderLen; idx += orderLen;
break; break;
@@ -571,7 +601,15 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27) case GocaConstants.G_GSVW_DEF:
case GocaConstants.G_GSVW: { // Set Viewing Window (0x23 / 0x27)
if (payloadLen >= 8 && idx + 9 < end) {
byte[] vwData = new byte[8];
System.arraycopy(inputData, idx + 2, vwData, 0, 8);
processViewingWindow(vwData);
} else if (payloadLen == 0 && plane != null) {
plane.clearViewingWindow();
}
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -632,6 +670,17 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GSCR: { // Set Character Shear (0x35) case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
if (payloadLen >= 4 && idx + 5 < end) {
int sx = readCoord(inputData, idx + 2);
int sy = readCoord(inputData, idx + 4);
if (sx != 0 || sy != 0) {
charShear = Math.toDegrees(Math.atan2(sx, sy));
}
} else if (payloadLen >= 2 && idx + 3 < end) {
int intPart = inputData[idx + 2];
int fracPart = (payloadLen >= 2) ? (inputData[idx + 3] & 0xFF) : 0;
charShear = (intPart + fracPart / 256.0) * 45.0;
}
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -655,8 +704,16 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSFLW: { // Set Fractional Line Width (0x11)
if (orderLen >= 2 && idx + 1 < end) {
byte[] flwData = new byte[orderLen - 1];
System.arraycopy(inputData, idx + 1, flwData, 0, flwData.length);
processFractionalLineWidth(flwData);
}
idx += orderLen;
break;
}
case 0x06: case 0x06:
case 0x11:
case GocaConstants.G_GSLT: { // Set Line Type (0x18) case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF; lineType = inputData[idx + 1] & 0xFF;
idx += orderLen; idx += orderLen;
@@ -704,16 +761,22 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GSCS: { // Set Character Set (0x38) case GocaConstants.G_GSCS: { // Set Character Set (0x38)
charSet = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); int cs = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
charSet = (cs == 0xF0) ? 0 : cs;
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSCC: { // Set Character Precision (0x3B) case GocaConstants.G_GSCC: { // Set Character Precision (0x39)
charPrecision = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); charPrecision = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
if (charPrecision == 0) charPrecision = GocaConstants.CP_STRING; if (charPrecision == 0) charPrecision = GocaConstants.CP_STRING;
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSMP: { // Set Marker Precision (0x3B)
markerPrecision = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
idx += orderLen;
break;
}
case GocaConstants.G_GSMX: case GocaConstants.G_GSMX:
case GocaConstants.G_GSMS_SET: case GocaConstants.G_GSMS_SET:
case GocaConstants.G_GPOP: { case GocaConstants.G_GPOP: {
@@ -729,8 +792,9 @@ public class GocaDecoder {
case GocaConstants.G_GBAR: { // Begin Area (0x68) case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0; boolean drawBoundary = (flags & 0x80) != 0;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary)); int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
beginArea(drawBoundary); logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
beginArea(drawBoundary, fillRule);
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -838,7 +902,18 @@ public class GocaDecoder {
int y = readCoord(inputData, idx + 4); int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6); int w = readCoord(inputData, idx + 6);
int h = readCoord(inputData, idx + 8); int h = readCoord(inputData, idx + 8);
beginImage(x, y, w, h); int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 9) {
int fmt = inputData[idx + 10] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 10) {
compression = inputData[idx + 11] & 0xFF;
}
beginImage(x, y, w, h, bitDepth, compression);
} }
idx += orderLen; idx += orderLen;
break; break;
@@ -901,8 +976,6 @@ public class GocaDecoder {
activeSegmentsInOrder.clear(); activeSegmentsInOrder.clear();
segmentBoundsMap.clear(); segmentBoundsMap.clear();
segmentStore.clear(); segmentStore.clear();
segmentOrderList.clear();
chainedTargets.clear();
logger.info("GOCA P_ERASE: erased graphics presentation space and cleared segment stores"); logger.info("GOCA P_ERASE: erased graphics presentation space and cleared segment stores");
idx += 2; idx += 2;
break; break;
@@ -925,11 +998,7 @@ public class GocaDecoder {
} }
break; break;
} }
case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position (HODGraphicCursorPosition - No-op per HOD architecture)
if (idx + 5 <= end) {
this.graphicCursorX = readCoord(data, idx + 2);
this.graphicCursorY = readCoord(data, idx + 4);
}
if (idx + 1 < end) { if (idx + 1 < end) {
int len = data[idx + 1] & 0xFF; int len = data[idx + 1] & 0xFF;
idx += 2 + len; idx += 2 + len;
@@ -961,16 +1030,21 @@ public class GocaDecoder {
} }
private void beginArea(boolean drawBoundary) { private void beginArea(boolean drawBoundary) {
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
}
private void beginArea(boolean drawBoundary, int fillRule) {
this.inArea = true; this.inArea = true;
this.areaDrawBoundary = drawBoundary; this.areaDrawBoundary = drawBoundary;
this.areaFillRule = fillRule;
this.areaFill = true; this.areaFill = true;
this.fillColor = this.curColor; this.fillColor = this.curColor;
this.areaPointsX.clear(); this.areaPointsX.clear();
this.areaPointsY.clear(); this.areaPointsY.clear();
this.areaPolygons.clear(); this.areaPolygons.clear();
this.currentPolyPts = 0; this.currentPolyPts = 0;
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d", logger.info(String.format("GOCA beginArea: drawBoundary=%b fillRule=%d fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillColor, patternSet, pattern)); drawBoundary, fillRule, fillColor, patternSet, pattern));
} }
private void endArea() { private void endArea() {
@@ -1002,11 +1076,11 @@ public class GocaDecoder {
polyCounts[i] = areaPolygons.get(i); polyCounts[i] = areaPolygons.get(i);
} }
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d", logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d fillRule=%d",
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix)); n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix, areaFillRule));
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet, plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor); pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor, areaFillRule);
inArea = false; inArea = false;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
@@ -1045,11 +1119,17 @@ public class GocaDecoder {
} }
private void beginImage(int x, int y, int w, int h) { private void beginImage(int x, int y, int w, int h) {
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
}
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
this.inImage = true; this.inImage = true;
this.imgX = x; this.imgX = x;
this.imgY = y; this.imgY = y;
this.imgWidth = w; this.imgWidth = w;
this.imgHeight = h; this.imgHeight = h;
this.imgBitDepth = bitDepth;
this.imgCompression = compression;
this.imgBuffer.clear(); this.imgBuffer.clear();
} }
@@ -1065,7 +1145,7 @@ public class GocaDecoder {
int px = plane.mapX(imgX); int px = plane.mapX(imgX);
int py = plane.mapY(imgY); int py = plane.mapY(imgY);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor); plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
inImage = false; inImage = false;
imgBuffer.clear(); imgBuffer.clear();
} }
@@ -1372,24 +1452,49 @@ public class GocaDecoder {
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0; double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0; double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
int totalW = textLen * (charWidth > 0 ? charWidth : 9); int cellW = (charWidth > 0 ? charWidth : 9);
int totalH = (charHeight > 0 ? charHeight : 14); int cellH = (charHeight > 0 ? charHeight : 16);
trackPoint(startX, startY); switch (charDir) {
trackPoint(startX + totalW, startY + totalH); case GocaConstants.CD_TB:
trackPoint(startX + totalW, startY - totalH); trackPoint(startX, startY);
trackPoint(startX + cellW, startY - textLen * cellH);
break;
case GocaConstants.CD_RL:
trackPoint(startX, startY);
trackPoint(startX - textLen * cellW, startY + cellH);
break;
case GocaConstants.CD_BT:
trackPoint(startX, startY);
trackPoint(startX + cellW, startY + textLen * cellH);
break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
trackPoint(startX, startY);
trackPoint(startX + textLen * cellW, startY + cellH);
break;
}
if (charSet != 0 && programSymbolManager != null) { if (charSet == 0xF8 || charPrecision == GocaConstants.CP_STROKE) {
char[] chars = new char[textLen];
for (int i = 0; i < textLen; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
}
String text = new String(chars);
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle, charShear);
} else if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) { for (int i = 0; i < textLen; i++) {
int code = data[pos + i] & 0xFF; int code = data[pos + i] & 0xFF;
double px = plane.mapXDouble(startX); double px = plane.mapXDouble(startX);
double py = plane.mapYDouble(startY) - ch; double py = plane.mapYDouble(startY);
ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code); ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code);
if (slot != null) { if (slot != null) {
int[] rgb = slot.getRgbPixels(curColor, 0); int[] rgb = slot.getRgbPixels(curColor, 0);
int symW = slot.getWidth(); int symW = slot.getWidth();
int symH = slot.getHeight(); int symH = slot.getHeight();
int ipx = (int) Math.round(px); int ipx = (int) Math.round(px);
int ipy = (int) Math.round(py); int ipy = (int) Math.round(py - ch);
int icw = (int) Math.round(cw); int icw = (int) Math.round(cw);
int ich = (int) Math.round(ch); int ich = (int) Math.round(ch);
for (int dy = 0; dy < ich; dy++) { for (int dy = 0; dy < ich; dy++) {
@@ -1402,44 +1507,57 @@ public class GocaDecoder {
} }
} }
} }
} else {
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
} }
startX += (charWidth > 0 ? charWidth : 9); startX += (charWidth > 0 ? charWidth : 9);
} }
curX = startX; curX = startX;
curY = startY; curY = startY;
return; return;
}
char[] chars = new char[textLen];
for (int i = 0; i < textLen; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
}
String text = new String(chars);
if (charPrecision == GocaConstants.CP_STROKE) {
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text,
curColor, cw, ch, charDir, charAngle);
} else { } else {
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text, char[] chars = new char[textLen];
curColor, cw, ch, charDir, charAngle); for (int i = 0; i < textLen; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
}
String text = new String(chars);
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle, charShear);
} }
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9)); switch (charDir) {
curY = startY; case GocaConstants.CD_TB:
curX = startX;
curY = startY - (textLen * (charHeight > 0 ? charHeight : 16));
break;
case GocaConstants.CD_RL:
curX = startX - (textLen * (charWidth > 0 ? charWidth : 9));
curY = startY;
break;
case GocaConstants.CD_BT:
curX = startX;
curY = startY + (textLen * (charHeight > 0 ? charHeight : 16));
break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
curY = startY;
break;
}
} }
/** /**
* Draws a transformed character string (matching HODDecoder.drawGCS). * Draws a transformed character string (matching HODDecoder.drawGCS).
*/ */
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) { public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) {
drawGcs(x, y, text, color, cw, ch, dir, angle, 0.0);
}
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle, double shearAngle) {
if (plane != null && text != null && !text.isEmpty()) { if (plane != null && text != null && !text.isEmpty()) {
if (charPrecision == GocaConstants.CP_STROKE) { if (charPrecision == GocaConstants.CP_STROKE) {
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle); plane.drawVectorText(x, y, text, color, cw, ch, dir, angle, shearAngle);
} else { } else {
plane.drawText(x, y, text, color, cw, ch, dir, angle); plane.drawText(x, y, text, color, cw, ch, dir, angle, shearAngle);
} }
} }
} }
@@ -1448,12 +1566,16 @@ public class GocaDecoder {
* Draws an EBCDIC byte buffer as a transformed character string. * Draws an EBCDIC byte buffer as a transformed character string.
*/ */
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) { public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) {
drawGcs(ebcdicData, offset, length, color, cw, ch, dir, angle, 0.0);
}
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle, double shearAngle) {
if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return; if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
char[] chars = new char[length]; char[] chars = new char[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]); chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]);
} }
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle); drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle, shearAngle);
} }
/** /**
@@ -106,4 +106,11 @@ public class GraphicInputBuilder {
return sf; return sf;
} }
/**
* Builds the 56-byte Graphic Input Structured Field for pick correlation with aperture.
*/
public static byte[] buildPickCorrelation(int gocaX, int gocaY, int aperture) {
return buildGraphicInput(gocaX, gocaY, 1, true, false, false);
}
} }
@@ -148,6 +148,7 @@ public class GraphicsPlane {
this.canvasWidth = w; this.canvasWidth = w;
this.canvasHeight = h; this.canvasHeight = h;
this.rgbBuffer = newBuffer; this.rgbBuffer = newBuffer;
updateViewingWindowPixels();
} }
public synchronized void clear() { public synchronized void clear() {
@@ -170,6 +171,59 @@ public class GraphicsPlane {
return rgbBuffer; return rgbBuffer;
} }
private int viewingWindowXMin = 0;
private int viewingWindowYMin = 0;
private int viewingWindowXMax = 0;
private int viewingWindowYMax = 0;
private boolean viewingWindowActive = false;
private int clipPixelXMin = 0;
private int clipPixelYMin = 0;
private int clipPixelXMax = 0;
private int clipPixelYMax = 0;
private double fractionalLineWidth = 1.0;
public synchronized void setViewingWindow(int xMin, int yMin, int xMax, int yMax) {
this.viewingWindowXMin = xMin;
this.viewingWindowYMin = yMin;
this.viewingWindowXMax = xMax;
this.viewingWindowYMax = yMax;
this.viewingWindowActive = true;
updateViewingWindowPixels();
}
public synchronized void clearViewingWindow() {
this.viewingWindowActive = false;
}
public synchronized boolean isViewingWindowActive() {
return viewingWindowActive;
}
public synchronized int getViewingWindowXMin() { return viewingWindowXMin; }
public synchronized int getViewingWindowYMin() { return viewingWindowYMin; }
public synchronized int getViewingWindowXMax() { return viewingWindowXMax; }
public synchronized int getViewingWindowYMax() { return viewingWindowYMax; }
public synchronized void setFractionalLineWidth(double flw) {
this.fractionalLineWidth = Math.max(0.1, flw);
}
public synchronized double getFractionalLineWidth() {
return fractionalLineWidth;
}
private void updateViewingWindowPixels() {
if (!viewingWindowActive) return;
int px1 = mapX(viewingWindowXMin);
int px2 = mapX(viewingWindowXMax);
int py1 = mapY(viewingWindowYMin);
int py2 = mapY(viewingWindowYMax);
this.clipPixelXMin = Math.max(0, Math.min(px1, px2));
this.clipPixelXMax = Math.min(canvasWidth - 1, Math.max(px1, px2));
this.clipPixelYMin = Math.max(0, Math.min(py1, py2));
this.clipPixelYMax = Math.min(canvasHeight - 1, Math.max(py1, py2));
}
public int getCanvasWidth() { public int getCanvasWidth() {
return canvasWidth; return canvasWidth;
} }
@@ -313,6 +367,11 @@ public class GraphicsPlane {
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending. * Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
*/ */
public synchronized void setPixel(int x, int y, int colorArgb) { public synchronized void setPixel(int x, int y, int colorArgb) {
if (viewingWindowActive) {
if (x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax) {
return;
}
}
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
int srcA = (colorArgb >>> 24) & 0xFF; int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return; if (srcA == 0) return;
@@ -447,11 +506,15 @@ public class GraphicsPlane {
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) { private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
if (brightness <= 0.0) return; if (brightness <= 0.0) return;
if (lineWidth == GocaConstants.LW_THICK) { if (lineWidth == GocaConstants.LW_THICK || fractionalLineWidth >= 1.5) {
int extra = (int) Math.round(Math.max(1, fractionalLineWidth - 0.5));
setPixelCoverage(x, y, colorRgb, 1.0); setPixelCoverage(x, y, colorRgb, 1.0);
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness)); for (int dx = -extra; dx <= extra; dx++) {
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness)); for (int dy = -extra; dy <= extra; dy++) {
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7)); if (dx == 0 && dy == 0) continue;
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
}
}
} else { } else {
// Perceptual gamma correction for crisp contrast on dark backgrounds // Perceptual gamma correction for crisp contrast on dark backgrounds
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15); double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
@@ -641,6 +704,9 @@ public class GraphicsPlane {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb); fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
} }
/**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/
/** /**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization. * Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/ */
@@ -648,25 +714,41 @@ public class GraphicsPlane {
int fillColorArgb, int patternSet, int pattern, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) { int bgMix, int bgColorArgb) {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, patternSet, pattern,
drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, GocaConstants.FILL_RULE_EVEN_ODD);
}
private static class NodeInter implements Comparable<NodeInter> {
final int x;
final int dir;
NodeInter(int x, int dir) {
this.x = x;
this.dir = dir;
}
@Override
public int compareTo(NodeInter o) {
return Integer.compare(this.x, o.x);
}
}
/**
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, int fillRule) {
if (px == null || py == null || numPoints < 3) return; if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb; int bg = bgColorArgb;
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
// Background Mix (GSBMX / bgMix):
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) && boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0); (bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
// Find polygon vertical bounds across all points
int minY = py[0]; int minY = py[0];
int maxY = py[0]; int maxY = py[0];
for (int i = 1; i < numPoints; i++) { for (int i = 1; i < numPoints; i++) {
@@ -676,7 +758,7 @@ public class GraphicsPlane {
minY = Math.max(0, minY); minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY); maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>(); List<NodeInter> nodeIntersections = new ArrayList<>();
byte[] patRows = null; byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null; ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && programSymbolManager != null) { if (patternSet >= 0x40 && programSymbolManager != null) {
@@ -691,7 +773,7 @@ public class GraphicsPlane {
} }
for (int y = minY; y <= maxY; y++) { for (int y = minY; y <= maxY; y++) {
nodeX.clear(); nodeIntersections.clear();
int offset = 0; int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1; int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) { for (int p = 0; p < polyCount; p++) {
@@ -705,7 +787,8 @@ public class GraphicsPlane {
int xj = px[offset + j]; int xj = px[offset + j];
if ((yi < y && yj >= y) || (yj < y && yi >= y)) { if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi)); int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
nodeX.add(x); int dir = (yj > yi) ? 1 : -1;
nodeIntersections.add(new NodeInter(x, dir));
} }
j = i; j = i;
} }
@@ -713,35 +796,72 @@ public class GraphicsPlane {
offset += pLen; offset += pLen;
} }
Collections.sort(nodeX); Collections.sort(nodeIntersections);
for (int i = 0; i < nodeX.size(); i += 2) { if (fillRule == GocaConstants.FILL_RULE_WINDING) {
if (i + 1 >= nodeX.size()) break; int winding = 0;
int leftX = Math.max(0, nodeX.get(i)); for (int i = 0; i < nodeIntersections.size() - 1; i++) {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); winding += nodeIntersections.get(i).dir;
if (winding != 0) {
int leftX = Math.max(0, nodeIntersections.get(i).x);
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
for (int x = leftX; x <= rightX; x++) { for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) { if (psSlot != null) {
int psW = psSlot.getWidth(); int psW = psSlot.getWidth();
int psH = psSlot.getHeight(); int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0; int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0; int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData(); byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg); setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
}
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { }
setPixel(x, y, fill); }
} else { } else {
int b = patRows[y & 7] & 0xFF; for (int i = 0; i < nodeIntersections.size(); i += 2) {
if (((b >> (7 - (x & 7))) & 1) != 0) { if (i + 1 >= nodeIntersections.size()) break;
int leftX = Math.max(0, nodeIntersections.get(i).x);
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background) } else {
setPixel(x, y, bg); int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} }
} }
} }
@@ -760,11 +880,6 @@ public class GraphicsPlane {
(double) px[offset + i + 1], (double) py[offset + i + 1], (double) px[offset + i + 1], (double) py[offset + i + 1],
boundaryColorArgb, lineType, lineWidth); boundaryColorArgb, lineType, lineWidth);
} }
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
(double) px[offset], (double) py[offset],
boundaryColorArgb, lineType, lineWidth);
}
} }
offset += pLen; offset += pLen;
} }
@@ -862,19 +977,29 @@ public class GraphicsPlane {
*/ */
public synchronized void drawText(double x, double y, String text, int colorArgb, public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) { double cellWidth, double cellHeight, int dir, double angle) {
drawText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
}
public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
if (textRenderer != null) { if (textRenderer != null) {
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle); textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
hasContent = true; hasContent = true;
updateCount++; updateCount++;
} else { } else {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle); drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
} }
} }
public synchronized void drawText(int x, int y, String text, int colorArgb, public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) { int cellWidth, int cellHeight, int dir, double angle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle); drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
} }
/** /**
@@ -882,27 +1007,49 @@ public class GraphicsPlane {
*/ */
public synchronized void drawVectorText(double x, double y, String text, int colorArgb, public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) { double cellWidth, double cellHeight, int dir, double angle) {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
}
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
double curX = x;
double curY = y;
double cw = cellWidth > 0 ? cellWidth : 12.0; double cw = cellWidth > 0 ? cellWidth : 12.0;
double ch = cellHeight > 0 ? cellHeight : 20.0; double ch = cellHeight > 0 ? cellHeight : 20.0;
double curX = x;
double curY = y;
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++) { for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i); char c = text.charAt(i);
drawVssChar(curX, curY, c, color, cw, ch); drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
switch (dir) { if (angle != 0.0) {
case GocaConstants.CD_TB: curY += ch; break; curX += cw * cosA;
case GocaConstants.CD_RL: curX -= cw; break; curY += cw * sinA;
case GocaConstants.CD_BT: curY -= ch; break; } else {
case GocaConstants.CD_LR: switch (dir) {
case GocaConstants.CD_DEFAULT: case GocaConstants.CD_TB: curY += ch; break;
default: case GocaConstants.CD_RL: curX -= cw; break;
curX += cw; case GocaConstants.CD_BT: curY -= ch; break;
break; case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
}
} }
} }
hasContent = true; hasContent = true;
@@ -911,10 +1058,19 @@ public class GraphicsPlane {
public synchronized void drawVectorText(int x, int y, String text, int colorArgb, public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) { int cellWidth, int cellHeight, int dir, double angle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle); drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
} }
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) { private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch, double angle, double shearAngle) {
int code = (int) c; int code = (int) c;
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) { if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
return; return;
@@ -924,6 +1080,11 @@ public class GraphicsPlane {
return; return;
} }
double radAngle = Math.toRadians(angle);
double cosA = Math.cos(radAngle);
double sinA = Math.sin(radAngle);
double tanShear = Math.tan(Math.toRadians(shearAngle));
int ptr = offset; int ptr = offset;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) { while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF; int order = VectorSymbolData.vss_data[ptr] & 0xFF;
@@ -941,8 +1102,18 @@ public class GraphicsPlane {
for (int p = 0; p < numPoints; p++) { for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF); int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF); int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
py[p] = y + ((double) (VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch; double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
double sx = nx - ny * tanShear;
double sy = ny;
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
px[p] = x + rx;
py[p] = y + ry;
ipx[p] = (int) Math.round(px[p]); ipx[p] = (int) Math.round(px[p]);
ipy[p] = (int) Math.round(py[p]); ipy[p] = (int) Math.round(py[p]);
} }
@@ -970,21 +1141,91 @@ public class GraphicsPlane {
} }
/** /**
* Draws raw image pixel bitmap. * Draws raw image pixel bitmap with default 1-bit depth and uncompressed format.
*/ */
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) { public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
drawImage(x, y, width, height, imageData, fgColorArgb, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
}
/**
* Draws bitmap image with support for 1-bit, 2-bit, 4-bit, 8-bit depth and RLE decompression.
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
int bitDepth, int compressionMode) {
if (imageData == null || width <= 0 || height <= 0) return; if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0]; int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) { byte[] rawData = imageData;
int rowOffset = row * bytesPerRow; if (compressionMode == GocaConstants.IMG_RLE) {
for (int col = 0; col < width; col++) { rawData = decompressGocaRle(imageData, width, height, bitDepth);
int byteIdx = rowOffset + (col / 8); }
if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0; int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
if (bit) {
setPixel(x + col, y + row, fgColor); if (depth == 1) {
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / 8);
if (byteIdx < rawData.length) {
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
}
}
}
}
} else if (depth == 2) {
int pixelsPerByte = 4;
int bytesPerRow = (width + 3) / 4;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / pixelsPerByte);
if (byteIdx < rawData.length) {
int shift = (3 - (col % 4)) * 2;
int val = (rawData[byteIdx] >> shift) & 0x03;
if (val != 0) {
int pixelColor;
switch (val) {
case 1: pixelColor = GocaConstants.GOCA_COLORS[1]; break; // Blue
case 2: pixelColor = GocaConstants.GOCA_COLORS[2]; break; // Red
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
default: pixelColor = fgColor; break;
}
setPixel(x + col, y + row, pixelColor);
}
}
}
}
} else if (depth == 4) {
int pixelsPerByte = 2;
int bytesPerRow = (width + 1) / 2;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / pixelsPerByte);
if (byteIdx < rawData.length) {
int shift = (1 - (col % 2)) * 4;
int val = (rawData[byteIdx] >> shift) & 0x0F;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
}
}
}
}
} else if (depth == 8) {
int bytesPerRow = width;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + col;
if (byteIdx < rawData.length) {
int val = rawData[byteIdx] & 0xFF;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
}
} }
} }
} }
@@ -992,4 +1233,35 @@ public class GraphicsPlane {
hasContent = true; hasContent = true;
updateCount++; updateCount++;
} }
/**
* Decompresses IBM GOCA Run-Length Encoded (RLE) bitmap raster streams.
*/
public static byte[] decompressGocaRle(byte[] rleData, int width, int height, int bitDepth) {
if (rleData == null || rleData.length == 0) return new byte[0];
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
int bytesPerRow = (width * depth + 7) / 8;
int expectedTotalBytes = bytesPerRow * height;
byte[] out = new byte[expectedTotalBytes];
int outIdx = 0;
int inIdx = 0;
while (inIdx < rleData.length && outIdx < expectedTotalBytes) {
int count = rleData[inIdx++] & 0xFF;
if (count == 0) {
if (inIdx < rleData.length) {
int litLen = rleData[inIdx++] & 0xFF;
for (int k = 0; k < litLen && inIdx < rleData.length && outIdx < expectedTotalBytes; k++) {
out[outIdx++] = rleData[inIdx++];
}
}
} else if (inIdx < rleData.length) {
byte val = rleData[inIdx++];
for (int k = 0; k < count && outIdx < expectedTotalBytes; k++) {
out[outIdx++] = val;
}
}
}
return out;
}
} }
@@ -212,15 +212,14 @@ public class ProgramSymbolManager {
int remaining = data.length - offset; int remaining = data.length - offset;
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint; int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int bytesPerSymbol; int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (loadFormat == 1) { if (sliceBytes <= 0) sliceBytes = 18;
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
} else {
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
}
if (bytesPerSymbol <= 0) { int bytesPerSymbol;
bytesPerSymbol = 18; if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
bytesPerSymbol = sliceBytes * 3;
} else {
bytesPerSymbol = sliceBytes;
} }
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) { while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
@@ -230,10 +229,23 @@ public class ProgramSymbolManager {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length)); System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
} }
if (loadFormat == 1) { if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane); // 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
}
} else { } else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane); if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
} }
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane)); set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
package haus.nightmare.lib3270j.listener;
/**
* Listener interface for receiving inbound SCS (SNA Character String) data streams
* transmitted by the host in TN3270E mode (Data Type DT_SCS_DATA = 0x01).
*/
public interface SCSInboundListener {
/**
* Called when an inbound SCS record is received from the host.
*
* @param data Raw SCS record payload bytes
* @param offset Start offset within buffer
* @param length Number of bytes in record
*/
void onSCSDataReceived(byte[] data, int offset, int length);
}
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 static final Logger log = Logger.getLogger(PD3270.class.getName());
private final PrinterConfig config; private final PrinterConfig config;
private PrinterDefinitionTable pdt;
private String destination; private String destination;
private boolean open = false; private boolean open = false;
@@ -38,6 +39,7 @@ public class PD3270 {
public PD3270(PrinterConfig config) { public PD3270(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig(); this.config = config != null ? config : new PrinterConfig();
this.pdt = this.config.getPrinterDefinitionTable();
this.destination = this.config.getDestinationTarget(); this.destination = this.config.getDestinationTarget();
} }
@@ -118,6 +120,13 @@ public class PD3270 {
if (!open) { if (!open) {
openPrinter(destination); openPrinter(destination);
} }
if (pdt != null) {
byte[] translated = pdt.translateChar(c);
if (translated != null && translated.length > 0) {
writePrintBytes(translated, 0, translated.length);
return;
}
}
try { try {
byte[] b = String.valueOf(c).getBytes(outputCharset); byte[] b = String.valueOf(c).getBytes(outputCharset);
memoryStream.write(b); memoryStream.write(b);
@@ -141,6 +150,12 @@ public class PD3270 {
if (!open) { if (!open) {
openPrinter(destination); openPrinter(destination);
} }
if (pdt != null) {
for (int i = 0; i < s.length(); i++) {
writePrintChar(s.charAt(i));
}
return;
}
try { try {
byte[] b = s.getBytes(outputCharset); byte[] b = s.getBytes(outputCharset);
memoryStream.write(b); memoryStream.write(b);
@@ -168,10 +183,149 @@ public class PD3270 {
*/ */
public synchronized void formFeed() { public synchronized void formFeed() {
pageCount++; pageCount++;
writePrintChar('\f'); if (pdt != null && pdt.hasControlCode(PrinterDefinitionTable.CMD_PAGE_FEED)) {
writeControlCode(PrinterDefinitionTable.CMD_PAGE_FEED);
} else {
writePrintChar('\f');
}
flush(); 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. * Flush all buffered print data to target stream.
*/ */
@@ -270,4 +424,12 @@ public class PD3270 {
this.outputCharset = charset; 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; 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) { public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig(); this.config = config != null ? config : new PrinterConfig();
this.pd = pd != null ? pd : new PD3270(this.config); this.pd = pd != null ? pd : new PD3270(this.config);
@@ -251,7 +255,48 @@ public class PrintPS3270 {
} }
if (startPrint) { if (startPrint) {
cancelAutoFlush();
flushPrintBuffer(); 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; 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. * Process host data stream containing SCS orders and characters.
* @param data Byte array from host. * @param data Byte array from host.
@@ -108,8 +128,6 @@ public class PrintSCS3270 {
int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0; int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0;
int orderTotalLen = 2 + (paramLen > 0 ? (paramLen + 1) : 1); // 0x2B + SubOrder + paramLen + payload 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 bytesAvailable = end - idx;
int sliceLen = Math.min(orderTotalLen, bytesAvailable); int sliceLen = Math.min(orderTotalLen, bytesAvailable);
@@ -126,6 +144,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_STO: case PrinterConstants.SCS_STO:
processSetTextOrientation(data, idx, sliceLen); processSetTextOrientation(data, idx, sliceLen);
break; break;
case PrinterConstants.SCS_SCS:
processSelectCharacterSet(data, idx, sliceLen);
break;
case PrinterConstants.SCS_SEAC: case PrinterConstants.SCS_SEAC:
processSetEnhancedAttribute(data, idx, sliceLen); processSetEnhancedAttribute(data, idx, sliceLen);
break; break;
@@ -135,6 +156,9 @@ public class PrintSCS3270 {
case PrinterConstants.SCS_PPV: case PrinterConstants.SCS_PPV:
processPresentationPositionVertical(data, idx, sliceLen); processPresentationPositionVertical(data, idx, sliceLen);
break; break;
case PrinterConstants.SCS_GEA:
processSetGraphicErrorAction(data, idx, sliceLen);
break;
default: default:
log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder)); log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder));
break; break;
@@ -154,12 +178,12 @@ public class PrintSCS3270 {
continue; continue;
} }
// Check TRS (Transparent Stream 0x35) // Check TRN / TRS (Transparent Stream 0x35)
if (b == PrinterConstants.SCS_TRS) { if (b == PrinterConstants.SCS_TRN) {
if (idx + 1 < end) { if (idx + 1 < end) {
int trsLen = data[idx + 1] & 0xFF; int trsLen = data[idx + 1] & 0xFF;
int actualTrs = Math.min(trsLen, end - (idx + 2)); int actualTrs = Math.min(trsLen, end - (idx + 2));
processTransparentStream(data, idx + 2, actualTrs); processTRN(data, idx + 2, actualTrs);
idx += 2 + actualTrs; idx += 2 + actualTrs;
} else { } else {
idx = end; idx = end;
@@ -167,52 +191,83 @@ public class PrintSCS3270 {
continue; continue;
} }
// Check Single-Byte SCS Controls // Check Single-Byte SCS Controls (Full 28-command set)
switch (b) { switch (b) {
case PrinterConstants.SCS_NUL: case PrinterConstants.SCS_NUL:
// Null - ignored case PrinterConstants.SCS_NOP:
// Null / No-op - ignored
idx++; idx++;
break; 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: case PrinterConstants.SCS_CR:
carriageReturn(); case PrinterConstants.SCS_RCR:
processCR();
idx++; idx++;
break; break;
case PrinterConstants.SCS_LF: case PrinterConstants.SCS_LF:
lineFeed(); processLF();
idx++; idx++;
break; break;
case PrinterConstants.SCS_NL: case PrinterConstants.SCS_NL:
case PrinterConstants.SCS_RNLS: case PrinterConstants.SCS_RNLS:
newLine(); processNL();
idx++; idx++;
break; break;
case PrinterConstants.SCS_FF: case PrinterConstants.SCS_FF:
formFeed(); processFF();
idx++; idx++;
break; break;
case PrinterConstants.SCS_BS: case PrinterConstants.SCS_BS:
case PrinterConstants.SCS_NBS: case PrinterConstants.SCS_NBS:
backspace(); processBS();
idx++; idx++;
break; break;
case PrinterConstants.SCS_HT: case PrinterConstants.SCS_HT:
processHorizontalTab(); processHT();
idx++; idx++;
break; break;
case PrinterConstants.SCS_VT: case PrinterConstants.SCS_VT:
processVerticalTab(); processVT();
idx++;
break;
case PrinterConstants.SCS_SO:
processSO();
idx++;
break;
case PrinterConstants.SCS_SI:
processSI();
idx++; idx++;
break; break;
case PrinterConstants.SCS_ENP: case PrinterConstants.SCS_ENP:
presentationEnabled = true; processENP();
idx++; idx++;
break; break;
case PrinterConstants.SCS_INP: case PrinterConstants.SCS_INP:
presentationEnabled = false; processINP();
idx++;
break;
case PrinterConstants.SCS_POC:
processPOC(null);
idx++; idx++;
break; break;
case PrinterConstants.SCS_BEL: case PrinterConstants.SCS_BEL:
// Sound alarm processBEL();
idx++;
break;
case PrinterConstants.SCS_IRS:
processIRS();
idx++;
break;
case PrinterConstants.SCS_SUB:
processSUB();
idx++; idx++;
break; break;
case PrinterConstants.SCS_GE: case PrinterConstants.SCS_GE:
@@ -233,7 +288,17 @@ public class PrintSCS3270 {
// Standard printable character // Standard printable character
if (presentationEnabled) { if (presentationEnabled) {
char ch = translator.ebcdicToUnicode(b); 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++; idx++;
break; break;
@@ -293,10 +358,22 @@ public class PrintSCS3270 {
// ========== SCS Order Implementations ========== // ========== SCS Order Implementations ==========
public synchronized void carriageReturn() { public synchronized void carriageReturn() {
processCR();
}
public synchronized void processCR() {
currentCol = leftMargin; currentCol = leftMargin;
} }
public synchronized void processRCR() {
processCR();
}
public synchronized void lineFeed() { public synchronized void lineFeed() {
processLF();
}
public synchronized void processLF() {
flushLineBuffer(); flushLineBuffer();
currentRow++; currentRow++;
if (currentRow > bottomMargin || currentRow > mpl) { if (currentRow > bottomMargin || currentRow > mpl) {
@@ -305,11 +382,23 @@ public class PrintSCS3270 {
} }
public synchronized void newLine() { public synchronized void newLine() {
carriageReturn(); processNL();
lineFeed(); }
public synchronized void processNL() {
processCR();
processLF();
}
public synchronized void processRNL() {
processNL();
} }
public synchronized void formFeed() { public synchronized void formFeed() {
processFF();
}
public synchronized void processFF() {
flushLineBuffer(); flushLineBuffer();
pd.formFeed(); pd.formFeed();
currentRow = topMargin; currentRow = topMargin;
@@ -317,18 +406,139 @@ public class PrintSCS3270 {
} }
public synchronized void backspace() { public synchronized void backspace() {
processBS();
}
public synchronized void processBS() {
if (currentCol > leftMargin) { if (currentCol > leftMargin) {
currentCol -= (doubleWidth ? 2 : 1); currentCol -= (doubleWidth ? 2 : 1);
if (currentCol < leftMargin) currentCol = leftMargin; 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() { public synchronized void advanceToNextLine() {
newLine(); processNL();
} }
public synchronized void advanceToNextPage() { public synchronized void advanceToNextPage() {
formFeed(); processFF();
} }
// ========== Tab Stops and Calculations ========== // ========== Tab Stops and Calculations ==========
@@ -370,7 +580,7 @@ public class PrintSCS3270 {
int nextTab = calculateVerticalTab(currentRow); int nextTab = calculateVerticalTab(currentRow);
if (nextTab > 0 && nextTab <= bottomMargin) { if (nextTab > 0 && nextTab <= bottomMargin) {
while (currentRow < nextTab) { while (currentRow < nextTab) {
lineFeed(); processLF();
} }
} else { } else {
advanceToNextPage(); advanceToNextPage();
@@ -414,20 +624,59 @@ public class PrintSCS3270 {
} }
public synchronized void setPrintDensity(int cpi, int lpi) { public synchronized void setPrintDensity(int cpi, int lpi) {
if (cpi > 0) this.cpi = cpi; if (cpi > 0) {
if (lpi > 0) this.lpi = lpi; 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) { public synchronized void setEnhancedHighlight(int highlightType) {
this.activeHighlight = 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() { public synchronized void startDoubleWidthCharacters() {
this.doubleWidth = true; this.doubleWidth = true;
if (pd != null) pd.setDoubleWidth(true);
} }
public synchronized void endDoubleWidthCharacters() { public synchronized void endDoubleWidthCharacters() {
this.doubleWidth = false; this.doubleWidth = false;
if (pd != null) pd.setDoubleWidth(false);
} }
public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) { public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) {
@@ -484,7 +733,8 @@ public class PrintSCS3270 {
if (points > 0) { if (points > 0) {
// Line density in points / inch (72 points = 1 inch) // Line density in points / inch (72 points = 1 inch)
// 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI // 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) { public synchronized void processSetEnhancedAttribute(byte[] data, int offset, int len) {
if (len >= 5) { if (len >= 5) {
int attrVal = data[offset + 4] & 0xFF; int attrVal = data[offset + 4] & 0xFF;
@@ -520,11 +785,11 @@ public class PrintSCS3270 {
if (subfn == PrinterConstants.POS_ABSOLUTE) { if (subfn == PrinterConstants.POS_ABSOLUTE) {
while (currentRow < val && currentRow < bottomMargin) { while (currentRow < val && currentRow < bottomMargin) {
lineFeed(); processLF();
} }
} else if (subfn == PrinterConstants.POS_RELATIVE) { } else if (subfn == PrinterConstants.POS_RELATIVE) {
for (int i = 0; i < val && currentRow < bottomMargin; i++) { for (int i = 0; i < val && currentRow < bottomMargin; i++) {
lineFeed(); processLF();
} }
} }
} }
@@ -536,19 +801,23 @@ public class PrintSCS3270 {
if (attrType == PrinterConstants.SA_COLOR) { if (attrType == PrinterConstants.SA_COLOR) {
this.activeColor = attrVal; this.activeColor = attrVal;
} else if (attrType == PrinterConstants.SA_HILITE) { } else if (attrType == PrinterConstants.SA_HILITE || attrType == PrinterConstants.SA_EXT_HILITE) {
this.activeHighlight = attrVal; setEnhancedHighlight(attrVal);
} else if (attrType == PrinterConstants.SA_CHARSET) {
processSCS(attrVal);
} else if (attrType == PrinterConstants.SA_RESET) { } else if (attrType == PrinterConstants.SA_RESET) {
this.activeColor = 0; this.activeColor = 0;
this.activeHighlight = PrinterConstants.SEAC_DEFAULT; setEnhancedHighlight(PrinterConstants.SEAC_DEFAULT);
this.doubleWidth = false; endDoubleWidthCharacters();
} }
} }
public synchronized void processTransparentStream(byte[] data, int offset, int len) { public int getChannelLine(int channel) {
if (len > 0) { return config.getChannelLine(channel);
pd.writePrintBytes(data, offset, len); }
}
public void setChannelLine(int channel, int line) {
config.setChannelLine(channel, line);
} }
// ========== Accessors ========== // ========== Accessors ==========
@@ -571,13 +840,16 @@ public class PrintSCS3270 {
public int getCharsPerInch() { return cpi; } public int getCharsPerInch() { return cpi; }
public boolean isDoubleWidth() { return doubleWidth; } 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 int getActiveColor() { return activeColor; }
public void setActiveColor(int color) { this.activeColor = color; } public void setActiveColor(int color) { this.activeColor = color; }
public int getActiveHighlight() { return activeHighlight; } 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; } public int getTextOrientation() { return textOrientation; }
@@ -48,26 +48,44 @@ public class PrinterConfig {
private boolean autoFlushOnEoj = true; private boolean autoFlushOnEoj = true;
private boolean autoReconnect = false; 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) { public PrinterConfig(String host, int port) {
this();
this.host = host; this.host = host;
this.port = port; this.port = port;
} }
public PrinterConfig(String host, int port, String printerLuName) { public PrinterConfig(String host, int port, String printerLuName) {
this.host = host; this(host, port);
this.port = port;
this.printerLuName = printerLuName; this.printerLuName = printerLuName;
} }
public PrinterConfig(String host, int port, String printerLuName, boolean useTls) { public PrinterConfig(String host, int port, String printerLuName, boolean useTls) {
this.host = host; this(host, port, printerLuName);
this.port = port;
this.printerLuName = printerLuName;
this.useTls = useTls; 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 // Getters and Setters
public String getHost() { return host; } public String getHost() { return host; }
public void setHost(String host) { this.host = host; } public void setHost(String host) { this.host = host; }
@@ -125,6 +143,7 @@ public class PrinterConfig {
public void setMpl(int mpl) { public void setMpl(int mpl) {
this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL; this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL;
if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl; if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl;
initDefaultChannelTable();
} }
public int getLeftMargin() { return leftMargin; } public int getLeftMargin() { return leftMargin; }
@@ -159,4 +178,53 @@ public class PrinterConfig {
public boolean isAutoReconnect() { return autoReconnect; } public boolean isAutoReconnect() { return autoReconnect; }
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = 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) ========== // ========== SCS Single-Byte Control Codes (EBCDIC) ==========
public static final int SCS_NUL = 0x00; // Null 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_HT = 0x05; // Horizontal Tab
public static final int SCS_RNLS = 0x06; // Required New Line 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_RCR = 0x07; // Required Carriage Return
public static final int SCS_GE = 0x08; // Graphic Escape public static final int SCS_GE = 0x08; // Graphic Escape
public static final int SCS_VT = 0x0B; // Vertical Tab public static final int SCS_VT = 0x0B; // Vertical Tab
public static final int SCS_FF = 0x0C; // Form Feed public static final int SCS_FF = 0x0C; // Form Feed
public static final int SCS_CR = 0x0D; // Carriage Return 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_ENP = 0x14; // Enable Presentation
public static final int SCS_NL = 0x15; // New Line public static final int SCS_NL = 0x15; // New Line
public static final int SCS_BS = 0x16; // Backspace 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_INP = 0x24; // Inhibit Presentation
public static final int SCS_LF = 0x25; // Line Feed public static final int SCS_LF = 0x25; // Line Feed
public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm 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_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_SP = 0x40; // Space
public static final int SCS_RSP = 0x41; // Required 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>) public static final int SCS_SA = 0x28; // Set Attribute (0x28 <type> <val>)
// 0x2B Sub-orders // 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_SHF = 0xD1; // Set Horizontal Format
public static final int SCS_SVF = 0xD2; // Set Vertical 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_STO = 0xD3; // Set Text Orientation
public static final int SCS_SCS = 0xD4; // Select Character Set 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_SEAC = 0xD5; // Set Enhanced Attribute / Highlight
public static final int SCS_SLD = 0xD6; // Set Line Density 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 // SA Attribute Types
public static final int SA_RESET = 0x00; public static final int SA_RESET = 0x00;
public static final int SA_HILITE = 0x41; public static final int SA_HILITE = 0x41;
public static final int SA_COLOR = 0x42; public static final int SA_COLOR = 0x42;
public static final int SA_CHARSET = 0x43; public static final int SA_CHARSET = 0x43;
public static final int SA_EXT_HILITE = 0x45;
// SEAC Highlight Values // SEAC / Extended Highlight Values
public static final int SEAC_DEFAULT = 0x00; public static final int SEAC_DEFAULT = 0x00;
public static final int SEAC_BLINK = 0xF1; public static final int SEAC_NORMAL = 0x00;
public static final int SEAC_REVERSE = 0xF2; public static final int SEAC_BLINK = 0xF1;
public static final int SEAC_UNDERLINE = 0xF4; 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 // PPA/PPV Positioning Types
public static final int POS_ABSOLUTE = 0x01; // Absolute position (1-based) 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; readerThread = null;
} }
if (printPs != null) {
printPs.cancelAutoFlush();
}
pd.closePrinter(); pd.closePrinter();
updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected"); updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected");
} }
@@ -260,11 +264,17 @@ public class Telnet3270EP implements Runnable {
*/ */
public synchronized void sendEOJ(boolean isComplete) { public synchronized void sendEOJ(boolean isComplete) {
log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete); log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete);
if (config.isFormFeedAtEoj()) { if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
pd.formFeed(); printPs.processPrintComplete();
} } else {
if (config.isAutoFlushOnEoj()) { scs.flushLineBuffer();
pd.flush(); if (config.isFormFeedAtEoj()) {
pd.formFeed();
}
if (config.isAutoFlushOnEoj()) {
pd.flush();
}
pd.endJob();
} }
firePrintJobComplete(pd.getPageCount(), pd.getByteCount()); 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,33 +204,52 @@ public final class DS3270Constants {
public static final int SF_DESTROY_PART = 0x0d; public static final int SF_DESTROY_PART = 0x0d;
public static final int SF_ACTIVATE_PART = 0x0e; public static final int SF_ACTIVATE_PART = 0x0e;
public static final int SF_MODIFY_PART = 0x0f; public static final int SF_MODIFY_PART = 0x0f;
public static final int SF_SET_WINDOW = 0x0f; // Set Window (GOCA/Modify Partition)
public static final int SF_3270_GRAPHICS = 0x20; // 3270 Graphics / Object Control
public static final int SF_OBJECT_CONTROL = 0x20; // Object Control
public static final int SF_DOCUMENT_DATA = 0x24; // Document Data (embedded SCS / GOCA)
public static final int SF_DOC_DATA = 0x24; // Alias for Document Data
public static final int SF_OUTBOUND_DS = 0x40; public static final int SF_OUTBOUND_DS = 0x40;
public static final int SF_TRANSFER_DATA = 0xd0; public static final int SF_TRANSFER_DATA = 0xd0;
// ========== Query Reply codes ========== // ========== Query Reply codes ==========
public static final int QR_SUMMARY = 0x80; public static final int QR_SUMMARY = 0x80; // Summary
public static final int QR_USABLE_AREA = 0x81; public static final int QR_USABLE_AREA = 0x81; // Usable Area
public static final int QR_IMAGE = 0x82; public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
public static final int QR_TEXT_PART = 0x83; public static final int QR_TEXT_PART = 0x83; // Text Partitions
public static final int QR_ALPHA_PART = 0x84; public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
public static final int QR_CHARSETS = 0x85; public static final int QR_CHARSETS = 0x85; // Character Sets
public static final int QR_COLOR = 0x86; public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
public static final int QR_HIGHLIGHTING = 0x87; public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
public static final int QR_REPLY_MODES = 0x88; public static final int QR_REPLY_MODES = 0x88; // Reply Modes
public static final int QR_SAVE_RESTORE = 0x8c; public static final int QR_OUTLINING = 0x8c; // Field Outlining
public static final int QR_DBCS_ASIA = 0x91; public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
public static final int QR_DDM = 0x95; public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
public static final int QR_TRANSPARENCY = 0x99; public static final int QR_DDM = 0x95; // Distributed Data Management
public static final int QR_RPQNAMES = 0xa1; public static final int QR_AUXDA = 0x99; // Auxiliary Devices
public static final int QR_IMP_PART = 0xa6; public static final int QR_FILE = 0x9f; // File Transfer
public static final int QR_RPQ_NAMES = 0xa8; public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
public static final int QR_OEM_FMT = 0xb3; // OEM Format public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
public static final int QR_GCOLOR = 0xb4; // Graphic Color Table public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
public static final int QR_NULL = 0xff; 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 ========== // ========== Screen model sizes ==========
public static final int MODEL_2_ROWS = 24; public static final int MODEL_2_ROWS = 24;
@@ -115,4 +115,23 @@ public final class TelnetConstants {
default: return "CMD-" + cmd; default: return "CMD-" + cmd;
} }
} }
public static String qualifierName(int qual) {
switch (qual) {
case TELQUAL_IS: return "IS";
case TELQUAL_SEND: return "SEND";
case TELQUAL_INFO: return "INFO";
default: return "QUAL-" + qual;
}
}
/** NEW-ENVIRON object name lookup. */
public static String environObjectName(int obj) {
switch (obj) {
case TELOBJ_VAR: return "VAR";
case TELOBJ_VALUE: return "VALUE";
case TELOBJ_ESC: return "ESC";
case TELOBJ_USERVAR: return "USERVAR";
default: return "OBJ-" + obj;
}
}
} }
@@ -18,6 +18,20 @@ public class ExtendedAttribute {
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */ /** Background color (0x00 for default, or 0xf0-0xff for explicit). */
public byte bg; public byte bg;
// Character set constants
public static final byte CS_BASE = 0;
public static final byte CS_APL = 1;
public static final byte CS_LINEDRAW = 2;
public static final byte CS_DBCS = 3;
public static final byte CS_GE = 0x04;
// DBCS state constants
public static final byte DB_NONE = 0;
public static final byte DB_LEFT = 1; // Left / first half of double-byte char
public static final byte DB_RIGHT = 2; // Right / second half of double-byte char
public static final byte DB_SI = 3; // Shift-In control char
public static final byte DB_SO = 4; // Shift-Out control char
/** /**
* Graphics rendition bits. * Graphics rendition bits.
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08 * GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.screen;
import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator; import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLPS;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -39,6 +42,14 @@ public class ScreenBuffer {
private byte defaultGr = 0x00; private byte defaultGr = 0x00;
private byte defaultCs = 0x00; private byte defaultCs = 0x00;
private byte defaultIc = 0x00; private byte defaultIc = 0x00;
// Entry Assist / DOC mode state
private boolean docMode = false;
private boolean wordWrap = false;
private int docStartCol = 0;
private int docEndCol = -1;
private int[] tabStops = null;
private final EbcdicTranslator translator; private final EbcdicTranslator translator;
private final Object renderLock = new Object(); private final Object renderLock = new Object();
@@ -216,6 +227,10 @@ public class ScreenBuffer {
this.explicitPartitionActive = (pid != 0); this.explicitPartitionActive = (pid != 0);
} }
public synchronized PartitionInfo getPartition(int pid) {
return partitions.get(pid);
}
public synchronized void eraseReset(boolean alt) { public synchronized void eraseReset(boolean alt) {
partitions.clear(); partitions.clear();
this.activePartition = 0; this.activePartition = 0;
@@ -229,6 +244,11 @@ public class ScreenBuffer {
this.cursorAddress = addr; this.cursorAddress = addr;
this.displayCursorAddress = addr; this.displayCursorAddress = addr;
} }
public synchronized void setCursorPosition(int row, int col) {
int r = Math.max(0, Math.min(row, rows - 1));
int c = Math.max(0, Math.min(col, cols - 1));
setCursorAddress(r * cols + c);
}
public int getCursorRow() { return cursorAddress / cols; } public int getCursorRow() { return cursorAddress / cols; }
public int getCursorCol() { return cursorAddress % cols; } public int getCursorCol() { return cursorAddress % cols; }
@@ -264,7 +284,7 @@ public class ScreenBuffer {
public void setFieldAttribute(int pos, byte fa) { public void setFieldAttribute(int pos, byte fa) {
ExtendedAttribute ea = buffer[pos]; ExtendedAttribute ea = buffer[pos];
ea.clear(); ea.clear();
ea.fa = fa; ea.fa = (fa != 0) ? fa : (byte) FA_PRINTABLE;
if (!formatted) { if (!formatted) {
System.err.println("SCREEN BECAME FORMATTED at pos " + pos); System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
} }
@@ -466,39 +486,10 @@ public class ScreenBuffer {
} }
private char getAplGraphic(int ec) { private char getAplGraphic(int ec) {
switch (ec) { if (translator != null) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL) return translator.mapAPL(ec);
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return translator.ebcdicToUnicode(ec);
} }
return (char) (ec & 0xFF);
} }
/** /**
@@ -562,4 +553,608 @@ public class ScreenBuffer {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0; if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].fa; return buffer[baddr].fa;
} }
// ========== Phase 3: Field Management & Navigation ==========
/**
* Construct an ECLFieldList representation of the presentation space.
*/
public synchronized ECLFieldList buildFieldList() {
return new ECLFieldList(new ECLPS(this, null, translator), this);
}
/**
* Find the ECLField at the specified row and column.
*/
public ECLField findFieldAt(int row, int col) {
return buildFieldList().findFieldAt(row, col);
}
/**
* Find the ECLField containing the specified buffer position.
*/
public ECLField findFieldAt(int pos) {
return buildFieldList().findFieldAt(pos);
}
/**
* Find the ECLField containing the specified buffer position.
*/
public ECLField findField(int pos) {
return buildFieldList().findField(pos);
}
/**
* Find the field preceding the field at the given position.
*/
public ECLField findPrevField(int pos) {
return buildFieldList().findPrevField(pos);
}
/**
* Find the field succeeding the field at the given position.
*/
public ECLField findNextField(int pos) {
return buildFieldList().findNextField(pos);
}
/**
* Get the first field in the presentation space.
*/
public ECLField getFirstField() {
return buildFieldList().getFirstField();
}
// ========== Presentation Space Accessors & Convenience Methods ==========
public int getSize() {
return rows * cols;
}
public synchronized char getChar(int row, int col) {
int addr = rowColToAddress(row, col);
if (addr < 0 || addr >= rows * cols) return ' ';
ExtendedAttribute ea = buffer[addr];
if (ea.isFieldAttribute()) return ' ';
if (ea.ucs4 != 0) return (char) ea.ucs4;
if (ea.ec != 0) return translator.ebcdicToUnicode(ea.ec & 0xFF);
return ' ';
}
public synchronized void setChar(int row, int col, char c) {
int addr = rowColToAddress(row, col);
if (addr < 0 || addr >= rows * cols) return;
int ebc = translator.unicodeToEbcdic(c);
buffer[addr].ec = (byte) (ebc >= 0 ? ebc : 0);
buffer[addr].ucs4 = c;
screenChanged = true;
}
public synchronized void writeChar(int pos, byte ebc) {
if (pos < 0 || pos >= rows * cols) return;
buffer[pos].ec = ebc;
buffer[pos].ucs4 = translator.ebcdicToUnicode(ebc & 0xFF);
screenChanged = true;
}
public byte getAttr(int row, int col) {
return getFieldAttributeAt(rowColToAddress(row, col));
}
public ExtendedAttribute getExtAttr(int row, int col) {
return getCell(rowColToAddress(row, col));
}
public synchronized void setExtAttr(int row, int col, ExtendedAttribute ea) {
int addr = rowColToAddress(row, col);
if (addr >= 0 && addr < rows * cols && ea != null) {
buffer[addr].copyFrom(ea);
screenChanged = true;
}
}
public synchronized String getText() {
int size = rows * cols;
char[] buf = new char[size];
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (ea.isFieldAttribute()) {
buf[i] = ' ';
} else if (ea.ucs4 != 0) {
buf[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
buf[i] = ' ';
}
}
return new String(buf);
}
public synchronized String getString(int pos, int len) {
if (len <= 0) return "";
int size = rows * cols;
if (size <= 0) return "";
char[] buf = new char[len];
for (int i = 0; i < len; i++) {
int addr = (pos + i) % size;
ExtendedAttribute ea = buffer[addr];
if (ea.isFieldAttribute()) {
buf[i] = ' ';
} else if (ea.ucs4 != 0) {
buf[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
buf[i] = ' ';
}
}
return new String(buf);
}
public synchronized void setText(String text) {
if (text == null) return;
int size = rows * cols;
int len = Math.min(text.length(), size);
for (int i = 0; i < len; i++) {
char ch = text.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
buffer[i].ucs4 = ch;
}
screenChanged = true;
updateDisplaySnapshot();
}
public int searchString(String target) {
if (target == null || target.isEmpty()) return -1;
String full = getText();
return full.indexOf(target);
}
public boolean isModified() {
int size = rows * cols;
for (int i = 0; i < size; i++) {
if (buffer[i].isFieldAttribute() && faIsModified(buffer[i].fa & 0xFF)) {
return true;
}
}
return false;
}
public boolean isModified(int pos) {
return faIsModified(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isProtected(int pos) {
return faIsProtected(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isNumeric(int pos) {
return faIsNumeric(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isDisplay(int pos) {
return !faIsZero(getFieldAttributeAt(pos) & 0xFF);
}
// ========== DBCS Character Insertion & Deletion ==========
/**
* Insert a character at the specified buffer address with field boundary and DBCS preservation.
*/
public synchronized boolean insertChar(int pos, char ch) {
int size = rows * cols;
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (formatted) {
byte faVal = getFieldAttributeAt(pos);
if (faIsProtected(faVal & 0xFF)) return false;
}
boolean isDbcsChar = translator != null && translator.isDBCS() && translator.unicodeToDbcs(ch) >= 0;
int shiftAmount = isDbcsChar ? 2 : 1;
// Find end of field
int endAddr = pos;
int count = 0;
while (!getCell(incrementAddress(endAddr)).isFieldAttribute() && count < size) {
endAddr = incrementAddress(endAddr);
count++;
if (endAddr == pos) break;
}
// Check overflow
for (int s = 0; s < shiftAmount; s++) {
int checkAddr = endAddr;
for (int k = 0; k < s; k++) checkAddr = decrementAddress(checkAddr);
ExtendedAttribute eaEnd = getCell(checkAddr);
if (eaEnd.ec != 0 && eaEnd.ec != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
return false;
}
}
// Shift characters right
for (int s = 0; s < shiftAmount; s++) {
int dst = endAddr;
int shiftCount = 0;
while (dst != pos && shiftCount < size) {
int src = decrementAddress(dst);
getCell(dst).copyFrom(getCell(src));
dst = src;
shiftCount++;
}
getCell(pos).clear();
}
if (isDbcsChar) {
int dbcs = translator.unicodeToDbcs(ch);
int b1 = (dbcs >> 8) & 0xFF;
int b2 = dbcs & 0xFF;
int nextPos = incrementAddress(pos);
ExtendedAttribute ea1 = getCell(pos);
ea1.ec = (byte) b1;
ea1.ucs4 = ch;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ExtendedAttribute ea2 = getCell(nextPos);
ea2.ec = (byte) b2;
ea2.ucs4 = ch;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
setCursorAddress(incrementAddress(nextPos));
} else {
int ebc = translator.unicodeToEbcdic(ch);
ExtendedAttribute ea = getCell(pos);
ea.ec = (byte) (ebc >= 0 ? ebc : 0);
ea.ucs4 = ch;
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_NONE;
setCursorAddress(incrementAddress(pos));
}
// Set MDT
if (formatted) {
int faAddr = findFieldAttribute(pos);
if (faAddr >= 0) {
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
}
}
screenChanged = true;
updateDisplaySnapshot();
return true;
}
public synchronized boolean insertChar(char ch) {
return insertChar(cursorAddress, ch);
}
/**
* Delete a character at the specified buffer address, pulling trailing field text and preserving DBCS glyphs.
*/
public synchronized boolean deleteChar(int pos) {
int size = rows * cols;
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (getCell(pos).isFieldAttribute()) return false;
if (formatted) {
byte faVal = getFieldAttributeAt(pos);
if (faIsProtected(faVal & 0xFF)) return false;
}
ExtendedAttribute curCell = getCell(pos);
boolean isDbcs = curCell.db == ExtendedAttribute.DB_LEFT || curCell.db == ExtendedAttribute.DB_RIGHT
|| curCell.cs == ExtendedAttribute.CS_DBCS;
int deleteAmount = isDbcs ? 2 : 1;
if (curCell.db == ExtendedAttribute.DB_RIGHT) {
pos = decrementAddress(pos);
}
for (int d = 0; d < deleteAmount; d++) {
int shiftAddr = pos;
int count = 0;
while (count < size) {
int next = incrementAddress(shiftAddr);
if (getCell(next).isFieldAttribute()) {
getCell(shiftAddr).clear();
break;
}
getCell(shiftAddr).copyFrom(getCell(next));
shiftAddr = next;
count++;
}
}
cleanAdjacentSISO(pos);
if (formatted) {
int faAddr = findFieldAttribute(pos);
if (faAddr >= 0) {
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
}
}
screenChanged = true;
updateDisplaySnapshot();
return true;
}
public synchronized boolean deleteChar() {
return deleteChar(cursorAddress);
}
private void cleanAdjacentSISO(int nearPos) {
int size = rows * cols;
int start = Math.max(0, nearPos - 5);
int end = Math.min(size, nearPos + 10);
for (int i = start; i < end - 1; i++) {
ExtendedAttribute ea1 = getCell(i);
ExtendedAttribute ea2 = getCell(i + 1);
if (!ea1.isFieldAttribute() && !ea2.isFieldAttribute()) {
if ((ea1.ec & 0xFF) == 0x0E && (ea2.ec & 0xFF) == 0x0F) {
ea1.clear();
ea2.clear();
}
}
}
}
// ========== Entry Assist & DOC Mode Operations ==========
public boolean isEntryAssistDOCmode() { return docMode; }
public 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