Compare commits
2 Commits
v1.0.0
..
7b5bc7ec11
| Author | SHA1 | Date | |
|---|---|---|---|
|
7b5bc7ec11
|
|||
|
a9956298e2
|
+1
-1
@@ -118,7 +118,7 @@ public class TestRunner {
|
||||
public static void main(String[] args) {
|
||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
|
||||
.selectors(
|
||||
selectPackage("haus.nightmare.lib3270j")
|
||||
selectPackage("haus.nightmare")
|
||||
)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
buildUI();
|
||||
buildMenuBar();
|
||||
|
||||
ThemeManager.addThemeChangeListener(this::onThemeChanged);
|
||||
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
setMinimumSize(new Dimension(640, 400));
|
||||
@@ -97,10 +99,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
getContentPane().add(statusBar, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void onThemeChanged(UITheme theme) {
|
||||
buildMenuBar();
|
||||
statusBar.applyTheme(theme);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
|
||||
private void buildMenuBar() {
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
menuBar.setBackground(new Color(30, 30, 30));
|
||||
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
|
||||
ThemeManager.styleMenuBar(menuBar);
|
||||
|
||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
|
||||
@@ -145,6 +153,31 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}));
|
||||
viewMenu.addSeparator();
|
||||
|
||||
// UI Theme Submenu
|
||||
JMenu themeMenu = createMenu("UI Theme");
|
||||
ButtonGroup themeGroup = new ButtonGroup();
|
||||
|
||||
JRadioButtonMenuItem darkThemeItem = new JRadioButtonMenuItem("Dark Mode", ThemeManager.isDark());
|
||||
ThemeManager.styleMenuItem(darkThemeItem);
|
||||
darkThemeItem.addActionListener(e -> {
|
||||
haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.DARK);
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
});
|
||||
themeGroup.add(darkThemeItem);
|
||||
themeMenu.add(darkThemeItem);
|
||||
|
||||
JRadioButtonMenuItem lightThemeItem = new JRadioButtonMenuItem("Light Mode", ThemeManager.isLight());
|
||||
ThemeManager.styleMenuItem(lightThemeItem);
|
||||
lightThemeItem.addActionListener(e -> {
|
||||
haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
});
|
||||
themeGroup.add(lightThemeItem);
|
||||
themeMenu.add(lightThemeItem);
|
||||
|
||||
viewMenu.add(themeMenu);
|
||||
viewMenu.addSeparator();
|
||||
|
||||
// CodePage Submenu
|
||||
JMenu cpMenu = createMenu("Code Page");
|
||||
String[] codePages = {
|
||||
@@ -174,8 +207,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
for (String cpItem : codePages) {
|
||||
String cpId = cpItem.split(" -")[0].trim();
|
||||
JMenuItem cpMi = new JMenuItem(cpItem);
|
||||
cpMi.setBackground(new Color(40, 40, 40));
|
||||
cpMi.setForeground(new Color(200, 200, 200));
|
||||
ThemeManager.styleMenuItem(cpMi);
|
||||
cpMi.addActionListener(e -> changeCodePage(cpId));
|
||||
cpMenu.add(cpMi);
|
||||
}
|
||||
@@ -185,8 +217,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
JMenu gfxMenu = createMenu("Graphics Mode");
|
||||
for (GraphicsMode gm : GraphicsMode.values()) {
|
||||
JMenuItem gmMi = new JMenuItem(gm.name());
|
||||
gmMi.setBackground(new Color(40, 40, 40));
|
||||
gmMi.setForeground(new Color(200, 200, 200));
|
||||
ThemeManager.styleMenuItem(gmMi);
|
||||
gmMi.addActionListener(e -> changeGraphicsMode(gm));
|
||||
gfxMenu.add(gmMi);
|
||||
}
|
||||
@@ -257,14 +288,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
private JMenu createMenu(String name) {
|
||||
JMenu menu = new JMenu(name);
|
||||
menu.setForeground(new Color(200, 200, 200));
|
||||
return menu;
|
||||
return ThemeManager.styleMenu(menu);
|
||||
}
|
||||
|
||||
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
|
||||
JMenuItem item = new JMenuItem(name);
|
||||
item.setBackground(new Color(40, 40, 40));
|
||||
item.setForeground(new Color(200, 200, 200));
|
||||
ThemeManager.styleMenuItem(item);
|
||||
if (mnemonic > 0) {
|
||||
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
|
||||
@@ -693,9 +722,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
area.setBackground(new Color(30, 30, 30));
|
||||
area.setForeground(new Color(200, 200, 200));
|
||||
ThemeManager.styleTextArea(area);
|
||||
JScrollPane sp = new JScrollPane(area);
|
||||
ThemeManager.styleScrollPane(sp);
|
||||
sp.setPreferredSize(new Dimension(380, 430));
|
||||
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
@@ -726,9 +755,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
area.setBackground(new Color(30, 30, 30));
|
||||
area.setForeground(new Color(200, 200, 200));
|
||||
ThemeManager.styleTextArea(area);
|
||||
JScrollPane sp = new JScrollPane(area);
|
||||
ThemeManager.styleScrollPane(sp);
|
||||
sp.setPreferredSize(new Dimension(440, 430));
|
||||
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
@@ -758,45 +787,47 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
Boolean cliTn3270e = null;
|
||||
GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
|
||||
String arg = args[i];
|
||||
if ("-d".equals(arg) || "--debug".equals(arg)) {
|
||||
debug = true;
|
||||
} else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) {
|
||||
} else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
|
||||
cliTls = true;
|
||||
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
|
||||
} else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
|
||||
cliNoVerifyCert = true;
|
||||
} else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) {
|
||||
} else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
|
||||
cliTn3270e = false;
|
||||
} else if ("--tn3270e".equals(args[i])) {
|
||||
} else if ("--tn3270e".equals(arg)) {
|
||||
cliTn3270e = true;
|
||||
} else if (args[i].startsWith("--graphics=")) {
|
||||
cliGraphicsMode = GraphicsMode.fromString(args[i].substring(11));
|
||||
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
} else if (arg.startsWith("--graphics=")) {
|
||||
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
|
||||
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
|
||||
} else if ("--no-graphics".equals(args[i])) {
|
||||
} else if ("--no-graphics".equals(arg)) {
|
||||
cliGraphicsMode = GraphicsMode.NONE;
|
||||
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
|
||||
configFile = args[++i];
|
||||
} else if (arg.startsWith("-")) {
|
||||
System.err.println("Unknown option: " + arg);
|
||||
} else {
|
||||
remainingArgs.add(args[i]);
|
||||
remainingArgs.add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
Level logLevel = debug ? Level.ALL : Level.INFO;
|
||||
Level logLevel = debug ? Level.FINE : Level.INFO;
|
||||
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
}
|
||||
globalRoot.setLevel(Level.ALL);
|
||||
|
||||
java.util.logging.Filter appFilter = record -> {
|
||||
String name = record.getLoggerName();
|
||||
return name != null && (name.startsWith("haus.nightmare.lib3270j") || name.startsWith("haus.nightmare.j3270"));
|
||||
};
|
||||
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
||||
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(logLevel);
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
consoleHandler.setFormatter(new SimpleFormatter());
|
||||
consoleHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
@@ -837,6 +868,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
} catch (Exception e) {
|
||||
log.fine("Could not set system look and feel");
|
||||
}
|
||||
ThemeManager.applyUIManagerDefaults(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
|
||||
ThemeManager.setTheme(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
|
||||
|
||||
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
||||
System.setProperty("apple.awt.application.name", "j3270");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package haus.nightmare.j3270.config;
|
||||
|
||||
import haus.nightmare.j3270.ui.UITheme;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.awt.Color;
|
||||
import java.io.*;
|
||||
@@ -17,6 +18,15 @@ public class Settings {
|
||||
AUTO_CONNECT
|
||||
}
|
||||
|
||||
public static UITheme getJavaUiTheme() {
|
||||
String themeStr = prefs.get("javaUiTheme", UITheme.DARK.name());
|
||||
return UITheme.fromString(themeStr);
|
||||
}
|
||||
|
||||
public static void setJavaUiTheme(UITheme theme) {
|
||||
prefs.put("javaUiTheme", (theme != null ? theme : UITheme.DARK).name());
|
||||
}
|
||||
|
||||
public static String getFontFamily() {
|
||||
return prefs.get("fontFamily", "Monospaced");
|
||||
}
|
||||
@@ -229,6 +239,11 @@ public class Settings {
|
||||
switch (key) {
|
||||
case "fontFamily": setFontFamily(value); break;
|
||||
case "fontSize": setFontSize(Integer.parseInt(value)); break;
|
||||
case "javaUiTheme":
|
||||
case "theme":
|
||||
case "uiTheme":
|
||||
setJavaUiTheme(UITheme.fromString(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown appearance key: " + key);
|
||||
}
|
||||
@@ -322,6 +337,7 @@ public class Settings {
|
||||
|
||||
// [appearance]
|
||||
w.println("[appearance]");
|
||||
w.println("javaUiTheme = " + getJavaUiTheme().name());
|
||||
w.println("fontFamily = " + getFontFamily());
|
||||
w.println("fontSize = " + getFontSize());
|
||||
w.println();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||
|
||||
@@ -88,6 +89,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// Local File
|
||||
localFileField = new JTextField(20);
|
||||
browseLocalButton = new JButton("Browse...");
|
||||
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
|
||||
browseLocalButton.addActionListener(e -> browseLocalFile());
|
||||
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
|
||||
localPanel.setOpaque(false);
|
||||
@@ -98,6 +100,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// Host File
|
||||
hostFileField = new JTextField(20);
|
||||
browseHostButton = new JButton("Browse Host...");
|
||||
ThemeManager.styleButton(browseHostButton, ThemeManager.ButtonVariant.DEFAULT);
|
||||
browseHostButton.addActionListener(e -> browseHostDirectory());
|
||||
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
|
||||
hostPanel.setOpaque(false);
|
||||
@@ -154,10 +157,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// Host-specific options panel (TSO dataset allocation)
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
|
||||
hostOptsPanel.setOpaque(false);
|
||||
hostOptsPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
"TSO Allocation Options (Send Only)"));
|
||||
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
|
||||
|
||||
hostOptsPanel.add(new JLabel("RECFM:"));
|
||||
recfmField = new JTextField(5);
|
||||
@@ -181,10 +181,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// CMS options
|
||||
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
|
||||
vmOptsPanel.setOpaque(false);
|
||||
vmOptsPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
"CMS Options"));
|
||||
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
vmOptsPanel.setBorder(ThemeManager.createTitledBorder("CMS Options"));
|
||||
|
||||
optionsField = new JTextField(20);
|
||||
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
|
||||
@@ -200,9 +197,11 @@ public class FileTransferDialog extends JDialog {
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
transferButton = new JButton("Start Transfer");
|
||||
ThemeManager.styleButton(transferButton, ThemeManager.ButtonVariant.PRIMARY);
|
||||
transferButton.addActionListener(e -> startTransfer());
|
||||
|
||||
cancelButton = new JButton("Close");
|
||||
ThemeManager.styleButton(cancelButton, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelButton.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(cancelButton);
|
||||
@@ -212,7 +211,7 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
setContentPane(mainPanel);
|
||||
|
||||
applyTheme(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
updateOptionStates();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,24 +34,21 @@ public class ConnectDialog extends JDialog {
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new GridBagLayout());
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
mainPanel.setBackground(new Color(30, 30, 30));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
gbc.insets = new Insets(5, 6, 5, 6);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14);
|
||||
Color fg = new Color(200, 200, 200);
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Host
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
hostLabel.setForeground(fg);
|
||||
hostLabel.setFont(labelFont);
|
||||
mainPanel.add(hostLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
hostField = createDarkField(20);
|
||||
hostField = createField(20);
|
||||
mainPanel.add(hostField, gbc);
|
||||
|
||||
// Port
|
||||
@@ -59,12 +56,11 @@ public class ConnectDialog extends JDialog {
|
||||
gbc.gridy = 1;
|
||||
gbc.weightx = 0;
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
portLabel.setForeground(fg);
|
||||
portLabel.setFont(labelFont);
|
||||
mainPanel.add(portLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
portField = createDarkField(6);
|
||||
portField = createField(6);
|
||||
portField.setText("23");
|
||||
mainPanel.add(portField, gbc);
|
||||
|
||||
@@ -73,16 +69,14 @@ public class ConnectDialog extends JDialog {
|
||||
gbc.gridy = 2;
|
||||
gbc.weightx = 0;
|
||||
JLabel modelLabel = new JLabel("Model:");
|
||||
modelLabel.setForeground(fg);
|
||||
modelLabel.setFont(labelFont);
|
||||
mainPanel.add(modelLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
modelCombo = new JComboBox<>(TerminalModel.values());
|
||||
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
|
||||
modelCombo.setBackground(new Color(45, 45, 45));
|
||||
modelCombo.setForeground(fg);
|
||||
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
ThemeManager.styleComboBox(modelCombo);
|
||||
mainPanel.add(modelCombo, gbc);
|
||||
|
||||
// LU Name
|
||||
@@ -90,12 +84,11 @@ public class ConnectDialog extends JDialog {
|
||||
gbc.gridy = 3;
|
||||
gbc.weightx = 0;
|
||||
JLabel luLabel = new JLabel("LU Name:");
|
||||
luLabel.setForeground(fg);
|
||||
luLabel.setFont(labelFont);
|
||||
mainPanel.add(luLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
luField = createDarkField(12);
|
||||
luField = createField(12);
|
||||
mainPanel.add(luField, gbc);
|
||||
|
||||
// Graphics Mode
|
||||
@@ -103,16 +96,14 @@ public class ConnectDialog extends JDialog {
|
||||
gbc.gridy = 4;
|
||||
gbc.weightx = 0;
|
||||
JLabel graphicsLabel = new JLabel("Graphics:");
|
||||
graphicsLabel.setForeground(fg);
|
||||
graphicsLabel.setFont(labelFont);
|
||||
mainPanel.add(graphicsLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
graphicsCombo.setBackground(new Color(45, 45, 45));
|
||||
graphicsCombo.setForeground(fg);
|
||||
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
ThemeManager.styleComboBox(graphicsCombo);
|
||||
mainPanel.add(graphicsCombo, gbc);
|
||||
|
||||
// Code Page
|
||||
@@ -120,7 +111,6 @@ public class ConnectDialog extends JDialog {
|
||||
gbc.gridy = 5;
|
||||
gbc.weightx = 0;
|
||||
JLabel cpLabel = new JLabel("Code Page:");
|
||||
cpLabel.setForeground(fg);
|
||||
cpLabel.setFont(labelFont);
|
||||
mainPanel.add(cpLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
@@ -158,9 +148,8 @@ public class ConnectDialog extends JDialog {
|
||||
break;
|
||||
}
|
||||
}
|
||||
codePageCombo.setBackground(new Color(45, 45, 45));
|
||||
codePageCombo.setForeground(fg);
|
||||
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
ThemeManager.styleComboBox(codePageCombo);
|
||||
mainPanel.add(codePageCombo, gbc);
|
||||
|
||||
// TLS / SSL Checkbox
|
||||
@@ -168,10 +157,8 @@ public class ConnectDialog extends JDialog {
|
||||
gbc.gridy = 6;
|
||||
gbc.weightx = 1.0;
|
||||
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
||||
tlsCheckBox.setBackground(new Color(30, 30, 30));
|
||||
tlsCheckBox.setForeground(fg);
|
||||
ThemeManager.styleCheckBox(tlsCheckBox);
|
||||
tlsCheckBox.setFont(labelFont);
|
||||
tlsCheckBox.setFocusPainted(false);
|
||||
tlsCheckBox.addActionListener(e -> {
|
||||
boolean isTls = tlsCheckBox.isSelected();
|
||||
verifyCertCheckBox.setEnabled(isTls);
|
||||
@@ -188,38 +175,31 @@ public class ConnectDialog extends JDialog {
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 7;
|
||||
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
||||
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
|
||||
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
|
||||
ThemeManager.styleCheckBox(verifyCertCheckBox);
|
||||
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
verifyCertCheckBox.setSelected(true);
|
||||
verifyCertCheckBox.setEnabled(false);
|
||||
verifyCertCheckBox.setFocusPainted(false);
|
||||
mainPanel.add(verifyCertCheckBox, gbc);
|
||||
|
||||
// TN3270E Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 8;
|
||||
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
|
||||
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
|
||||
tn3270eCheckBox.setForeground(fg);
|
||||
ThemeManager.styleCheckBox(tn3270eCheckBox);
|
||||
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||
tn3270eCheckBox.setFocusPainted(false);
|
||||
mainPanel.add(tn3270eCheckBox, gbc);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
JButton connectBtn = new JButton("Connect");
|
||||
connectBtn.setBackground(new Color(50, 120, 50));
|
||||
connectBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
connectBtn.setFont(labelFont);
|
||||
connectBtn.addActionListener(e -> onConnect());
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel");
|
||||
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||
cancelBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.setFont(labelFont);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
confirmed = false;
|
||||
@@ -235,20 +215,16 @@ public class ConnectDialog extends JDialog {
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Enter key triggers connect
|
||||
getRootPane().setDefaultButton(connectBtn);
|
||||
}
|
||||
|
||||
private JTextField createDarkField(int cols) {
|
||||
private JTextField createField(int cols) {
|
||||
JTextField field = new JTextField(cols);
|
||||
field.setBackground(new Color(45, 45, 45));
|
||||
field.setForeground(new Color(200, 200, 200));
|
||||
field.setCaretColor(new Color(200, 200, 200));
|
||||
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
|
||||
field.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
|
||||
ThemeManager.styleTextField(field);
|
||||
return field;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,6 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
private JTable table;
|
||||
private JLabel countLabel;
|
||||
|
||||
private static final Color DARK_BG = new Color(35, 35, 35);
|
||||
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
|
||||
private static final Color DARK_FG = new Color(220, 220, 220);
|
||||
private static final Color DARK_BORDER = new Color(65, 65, 65);
|
||||
private static final Color DARK_SELECTION = new Color(75, 110, 175);
|
||||
|
||||
public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "3270 Presentation Space Field Inspector", false);
|
||||
this.client = client;
|
||||
@@ -61,19 +55,16 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
|
||||
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
mainPanel.setBackground(DARK_BG);
|
||||
|
||||
// Top info bar
|
||||
JPanel topPanel = new JPanel(new BorderLayout());
|
||||
topPanel.setOpaque(false);
|
||||
countLabel = new JLabel("0 fields detected on screen");
|
||||
countLabel.setForeground(DARK_FG);
|
||||
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
topPanel.add(countLabel, BorderLayout.WEST);
|
||||
|
||||
JButton refreshBtn = new JButton("Refresh");
|
||||
refreshBtn.setBackground(new Color(55, 55, 55));
|
||||
refreshBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(refreshBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
refreshBtn.addActionListener(e -> refreshFields());
|
||||
topPanel.add(refreshBtn, BorderLayout.EAST);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
@@ -91,18 +82,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
};
|
||||
|
||||
table = new JTable(tableModel);
|
||||
table.setBackground(DARK_FIELD_BG);
|
||||
table.setForeground(DARK_FG);
|
||||
table.setSelectionBackground(DARK_SELECTION);
|
||||
table.setSelectionForeground(Color.WHITE);
|
||||
table.setGridColor(DARK_BORDER);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(20);
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setBackground(new Color(50, 50, 50));
|
||||
header.setForeground(DARK_FG);
|
||||
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
|
||||
|
||||
// Column widths
|
||||
@@ -125,8 +110,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
scrollPane.setBorder(new LineBorder(DARK_BORDER));
|
||||
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom panel
|
||||
@@ -134,8 +118,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
JButton jumpBtn = new JButton("Jump to Selected Field");
|
||||
jumpBtn.setBackground(new Color(50, 100, 160));
|
||||
jumpBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(jumpBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
jumpBtn.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row >= 0) {
|
||||
@@ -144,8 +127,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
});
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(55, 55, 55));
|
||||
closeBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
bottomPanel.add(jumpBtn);
|
||||
@@ -153,6 +135,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
public synchronized void refreshFields() {
|
||||
|
||||
@@ -42,7 +42,6 @@ public class FindDialog extends JDialog {
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
mainPanel.setBackground(new Color(35, 35, 35));
|
||||
|
||||
// Form
|
||||
JPanel formPanel = new JPanel(new GridBagLayout());
|
||||
@@ -51,14 +50,12 @@ public class FindDialog extends JDialog {
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Color fg = new Color(220, 220, 220);
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Search text
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel findLabel = new JLabel("Find what:");
|
||||
findLabel.setForeground(fg);
|
||||
findLabel.setFont(labelFont);
|
||||
formPanel.add(findLabel, gbc);
|
||||
|
||||
@@ -66,23 +63,16 @@ public class FindDialog extends JDialog {
|
||||
gbc.weightx = 1.0;
|
||||
searchField = new JTextField(20);
|
||||
searchField.setText(lastSearchText);
|
||||
searchField.setBackground(new Color(50, 50, 50));
|
||||
searchField.setForeground(fg);
|
||||
searchField.setCaretColor(fg);
|
||||
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
searchField.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(new Color(70, 70, 70)),
|
||||
BorderFactory.createEmptyBorder(3, 6, 3, 6)));
|
||||
ThemeManager.styleTextField(searchField);
|
||||
formPanel.add(searchField, gbc);
|
||||
|
||||
// Options
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 1;
|
||||
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
|
||||
matchCaseCheck.setForeground(fg);
|
||||
matchCaseCheck.setOpaque(false);
|
||||
ThemeManager.styleCheckBox(matchCaseCheck);
|
||||
matchCaseCheck.setFont(labelFont);
|
||||
matchCaseCheck.setFocusPainted(false);
|
||||
formPanel.add(matchCaseCheck, gbc);
|
||||
|
||||
// Direction
|
||||
@@ -91,16 +81,11 @@ public class FindDialog extends JDialog {
|
||||
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
dirPanel.setOpaque(false);
|
||||
JLabel dirLabel = new JLabel("Direction: ");
|
||||
dirLabel.setForeground(fg);
|
||||
dirLabel.setFont(labelFont);
|
||||
forwardRadio = new JRadioButton("Down", lastForward);
|
||||
backwardRadio = new JRadioButton("Up", !lastForward);
|
||||
forwardRadio.setForeground(fg);
|
||||
backwardRadio.setForeground(fg);
|
||||
forwardRadio.setOpaque(false);
|
||||
backwardRadio.setOpaque(false);
|
||||
forwardRadio.setFocusPainted(false);
|
||||
backwardRadio.setFocusPainted(false);
|
||||
ThemeManager.styleRadioButton(forwardRadio);
|
||||
ThemeManager.styleRadioButton(backwardRadio);
|
||||
|
||||
ButtonGroup bg = new ButtonGroup();
|
||||
bg.add(forwardRadio);
|
||||
@@ -116,7 +101,7 @@ public class FindDialog extends JDialog {
|
||||
gbc.gridy = 3;
|
||||
gbc.gridwidth = 2;
|
||||
statusLabel = new JLabel(" ");
|
||||
statusLabel.setForeground(new Color(255, 120, 120));
|
||||
statusLabel.setForeground(ThemeManager.getOiaFgAlert());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
formPanel.add(statusLabel, gbc);
|
||||
|
||||
@@ -127,14 +112,12 @@ public class FindDialog extends JDialog {
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton findNextBtn = new JButton("Find Next");
|
||||
findNextBtn.setBackground(new Color(60, 63, 65));
|
||||
findNextBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(findNextBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
findNextBtn.setFont(labelFont);
|
||||
findNextBtn.addActionListener(e -> findNext());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(60, 63, 65));
|
||||
closeBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
@@ -144,6 +127,7 @@ public class FindDialog extends JDialog {
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
getRootPane().setDefaultButton(findNextBtn);
|
||||
|
||||
searchField.addKeyListener(new KeyAdapter() {
|
||||
|
||||
@@ -49,9 +49,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
private void buildUI(String initialQuery) {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
|
||||
mainPanel.setBackground(new Color(35, 35, 35));
|
||||
|
||||
Color fg = new Color(220, 220, 220);
|
||||
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Top Query bar
|
||||
@@ -62,7 +60,6 @@ public class HostDirectoryDialog extends JDialog {
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
JLabel envLabel = new JLabel("System:");
|
||||
envLabel.setForeground(fg);
|
||||
envLabel.setFont(font);
|
||||
topPanel.add(envLabel, gbc);
|
||||
|
||||
@@ -70,33 +67,25 @@ public class HostDirectoryDialog extends JDialog {
|
||||
hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS});
|
||||
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
|
||||
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
|
||||
hostTypeCombo.setBackground(new Color(50, 50, 50));
|
||||
hostTypeCombo.setForeground(fg);
|
||||
ThemeManager.styleComboBox(hostTypeCombo);
|
||||
topPanel.add(hostTypeCombo, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
JLabel queryLabel = new JLabel("Query Pattern / Text:");
|
||||
queryLabel.setForeground(fg);
|
||||
queryLabel.setFont(font);
|
||||
topPanel.add(queryLabel, gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.weightx = 1.0;
|
||||
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
|
||||
queryField.setBackground(new Color(50, 50, 50));
|
||||
queryField.setForeground(fg);
|
||||
queryField.setCaretColor(fg);
|
||||
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
queryField.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(new Color(70, 70, 70)),
|
||||
new EmptyBorder(3, 5, 3, 5)));
|
||||
ThemeManager.styleTextField(queryField);
|
||||
topPanel.add(queryField, gbc);
|
||||
|
||||
gbc.gridx = 4;
|
||||
gbc.weightx = 0;
|
||||
JButton parseBtn = new JButton("Query / Parse");
|
||||
parseBtn.setBackground(new Color(55, 55, 55));
|
||||
parseBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
parseBtn.setFont(font);
|
||||
parseBtn.addActionListener(e -> runQuery());
|
||||
topPanel.add(parseBtn, gbc);
|
||||
@@ -106,18 +95,12 @@ public class HostDirectoryDialog extends JDialog {
|
||||
// Table
|
||||
tableModel = new DefaultTableModel();
|
||||
table = new JTable(tableModel);
|
||||
table.setBackground(new Color(45, 45, 45));
|
||||
table.setForeground(fg);
|
||||
table.setSelectionBackground(new Color(75, 110, 175));
|
||||
table.setSelectionForeground(Color.WHITE);
|
||||
table.setGridColor(new Color(65, 65, 65));
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(20);
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setBackground(new Color(50, 50, 50));
|
||||
header.setForeground(fg);
|
||||
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
|
||||
|
||||
table.addMouseListener(new MouseAdapter() {
|
||||
@@ -130,8 +113,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
scrollPane.setBorder(new LineBorder(new Color(65, 65, 65)));
|
||||
scrollPane.getViewport().setBackground(new Color(45, 45, 45));
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom
|
||||
@@ -139,7 +121,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
|
||||
statusLabel.setForeground(new Color(170, 170, 170));
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
@@ -147,20 +129,17 @@ public class HostDirectoryDialog extends JDialog {
|
||||
btnPanel.setOpaque(false);
|
||||
|
||||
JButton pasteScreenBtn = new JButton("Parse Current Screen");
|
||||
pasteScreenBtn.setBackground(new Color(50, 50, 50));
|
||||
pasteScreenBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pasteScreenBtn.setFont(font);
|
||||
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
|
||||
|
||||
JButton selectBtn = new JButton("Select Dataset");
|
||||
selectBtn.setBackground(new Color(50, 120, 50));
|
||||
selectBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
selectBtn.setFont(font);
|
||||
selectBtn.addActionListener(e -> onConfirmSelection());
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel");
|
||||
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||
cancelBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.setFont(font);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
confirmed = false;
|
||||
@@ -176,6 +155,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Initial setup
|
||||
setupTableColumns();
|
||||
|
||||
@@ -45,10 +45,6 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
// Spool Display
|
||||
private JTextArea spoolArea;
|
||||
|
||||
private static final Color DARK_BG = new Color(35, 35, 35);
|
||||
private static final Color DARK_FG = new Color(220, 220, 220);
|
||||
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
|
||||
|
||||
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
|
||||
super(parent, "IBM 3287 Printer Session Manager", false);
|
||||
this.parent = parent;
|
||||
@@ -65,14 +61,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
mainPanel.setBackground(DARK_BG);
|
||||
|
||||
// Top: Configuration Panel
|
||||
JPanel topPanel = new JPanel(new GridBagLayout());
|
||||
topPanel.setOpaque(false);
|
||||
topPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
new LineBorder(new Color(65, 65, 65)), "Printer Session Configuration"));
|
||||
((TitledBorder) topPanel.getBorder()).setTitleColor(DARK_FG);
|
||||
topPanel.setBorder(ThemeManager.createTitledBorder("Printer Session Configuration"));
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(3, 4, 3, 4);
|
||||
@@ -83,60 +76,59 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
// Host / Port
|
||||
gbc.gridx = 0; gbc.gridy = 0;
|
||||
JLabel hLbl = new JLabel("Host:");
|
||||
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont);
|
||||
hLbl.setFont(labelFont);
|
||||
topPanel.add(hLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
hostField = createDarkField(config.getHost() != null ? config.getHost() : "localhost", 14);
|
||||
hostField = createField(config.getHost() != null ? config.getHost() : "localhost", 14);
|
||||
topPanel.add(hostField, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.weightx = 0;
|
||||
JLabel pLbl = new JLabel("Port:");
|
||||
pLbl.setForeground(DARK_FG); pLbl.setFont(labelFont);
|
||||
pLbl.setFont(labelFont);
|
||||
topPanel.add(pLbl, gbc);
|
||||
|
||||
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||
portField = createDarkField(String.valueOf(config.getPort()), 5);
|
||||
portField = createField(String.valueOf(config.getPort()), 5);
|
||||
topPanel.add(portField, gbc);
|
||||
|
||||
// Printer LU / Display LU
|
||||
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
|
||||
JLabel pluLbl = new JLabel("Printer LU:");
|
||||
pluLbl.setForeground(DARK_FG); pluLbl.setFont(labelFont);
|
||||
pluLbl.setFont(labelFont);
|
||||
topPanel.add(pluLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
printerLuField = createDarkField("", 10);
|
||||
printerLuField = createField("", 10);
|
||||
topPanel.add(printerLuField, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.weightx = 0;
|
||||
JLabel assocLbl = new JLabel("Assoc LU:");
|
||||
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont);
|
||||
assocLbl.setFont(labelFont);
|
||||
topPanel.add(assocLbl, gbc);
|
||||
|
||||
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||
displayLuField = createDarkField("", 10);
|
||||
displayLuField = createField("", 10);
|
||||
topPanel.add(displayLuField, gbc);
|
||||
|
||||
// CodePage & TLS
|
||||
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
|
||||
JLabel cpLbl = new JLabel("CodePage:");
|
||||
cpLbl.setForeground(DARK_FG); cpLbl.setFont(labelFont);
|
||||
cpLbl.setFont(labelFont);
|
||||
topPanel.add(cpLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"});
|
||||
codePageCombo.setBackground(DARK_FIELD_BG);
|
||||
codePageCombo.setForeground(DARK_FG);
|
||||
ThemeManager.styleComboBox(codePageCombo);
|
||||
topPanel.add(codePageCombo, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.gridwidth = 2;
|
||||
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
|
||||
tlsPanel.setOpaque(false);
|
||||
tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls());
|
||||
tlsCheck.setOpaque(false); tlsCheck.setForeground(DARK_FG); tlsCheck.setFocusPainted(false);
|
||||
ThemeManager.styleCheckBox(tlsCheck);
|
||||
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
|
||||
verifyCertCheck.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false);
|
||||
ThemeManager.styleCheckBox(verifyCertCheck);
|
||||
tlsPanel.add(tlsCheck);
|
||||
tlsPanel.add(verifyCertCheck);
|
||||
topPanel.add(tlsPanel, gbc);
|
||||
@@ -144,23 +136,22 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
// Destination Type & Target
|
||||
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
|
||||
JLabel destLbl = new JLabel("Destination:");
|
||||
destLbl.setForeground(DARK_FG); destLbl.setFont(labelFont);
|
||||
destLbl.setFont(labelFont);
|
||||
topPanel.add(destLbl, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
|
||||
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
|
||||
destinationCombo.setBackground(DARK_FIELD_BG);
|
||||
destinationCombo.setForeground(DARK_FG);
|
||||
ThemeManager.styleComboBox(destinationCombo);
|
||||
topPanel.add(destinationCombo, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
JLabel tgtLbl = new JLabel("Target Path:");
|
||||
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont);
|
||||
tgtLbl.setFont(labelFont);
|
||||
topPanel.add(tgtLbl, gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
targetField = createDarkField("printer_output.txt", 12);
|
||||
targetField = createField("printer_output.txt", 12);
|
||||
topPanel.add(targetField, gbc);
|
||||
|
||||
// Connect / Disconnect Buttons
|
||||
@@ -169,13 +160,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
connBtnPan.setOpaque(false);
|
||||
|
||||
connectBtn = new JButton("Start Printer Session");
|
||||
connectBtn.setBackground(new Color(50, 120, 50));
|
||||
connectBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
connectBtn.addActionListener(e -> startPrinterSession());
|
||||
|
||||
disconnectBtn = new JButton("Stop Session");
|
||||
disconnectBtn.setBackground(new Color(120, 50, 50));
|
||||
disconnectBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(disconnectBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
disconnectBtn.setEnabled(false);
|
||||
disconnectBtn.addActionListener(e -> stopPrinterSession());
|
||||
|
||||
@@ -188,9 +177,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
// Center: Spool Area & Status
|
||||
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
|
||||
centerPanel.setOpaque(false);
|
||||
centerPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
new LineBorder(new Color(65, 65, 65)), "Printer Spool & Status"));
|
||||
((TitledBorder) centerPanel.getBorder()).setTitleColor(DARK_FG);
|
||||
centerPanel.setBorder(ThemeManager.createTitledBorder("Printer Spool & Status"));
|
||||
|
||||
// Status Header
|
||||
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
|
||||
@@ -198,19 +185,16 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
|
||||
|
||||
statusLabel = new JLabel("Status: Disconnected");
|
||||
statusLabel.setForeground(new Color(180, 180, 180));
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
|
||||
sessionTypeLabel = new JLabel("Session: -");
|
||||
sessionTypeLabel.setForeground(DARK_FG);
|
||||
sessionTypeLabel.setFont(labelFont);
|
||||
|
||||
pagesLabel = new JLabel("Pages: 0");
|
||||
pagesLabel.setForeground(DARK_FG);
|
||||
pagesLabel.setFont(labelFont);
|
||||
|
||||
bytesLabel = new JLabel("Bytes: 0");
|
||||
bytesLabel.setForeground(DARK_FG);
|
||||
bytesLabel.setFont(labelFont);
|
||||
|
||||
statusHeader.add(statusLabel);
|
||||
@@ -221,14 +205,12 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
|
||||
// Spool text area
|
||||
spoolArea = new JTextArea();
|
||||
spoolArea.setBackground(new Color(25, 25, 25));
|
||||
spoolArea.setForeground(new Color(100, 255, 100));
|
||||
spoolArea.setCaretColor(Color.WHITE);
|
||||
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
spoolArea.setEditable(false);
|
||||
ThemeManager.styleTextArea(spoolArea);
|
||||
|
||||
JScrollPane spoolScroll = new JScrollPane(spoolArea);
|
||||
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60)));
|
||||
ThemeManager.styleScrollPane(spoolScroll);
|
||||
centerPanel.add(spoolScroll, BorderLayout.CENTER);
|
||||
|
||||
mainPanel.add(centerPanel, BorderLayout.CENTER);
|
||||
@@ -238,8 +220,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
JButton clearSpoolBtn = new JButton("Clear Spool");
|
||||
clearSpoolBtn.setBackground(new Color(50, 50, 50));
|
||||
clearSpoolBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(clearSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
clearSpoolBtn.addActionListener(e -> {
|
||||
spoolArea.setText("");
|
||||
if (printerSession != null && printerSession.getPD() != null) {
|
||||
@@ -250,18 +231,15 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
});
|
||||
|
||||
JButton saveSpoolBtn = new JButton("Save Spool As...");
|
||||
saveSpoolBtn.setBackground(new Color(50, 50, 50));
|
||||
saveSpoolBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(saveSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveSpoolBtn.addActionListener(e -> saveSpool());
|
||||
|
||||
JButton printSpoolBtn = new JButton("Print Spool...");
|
||||
printSpoolBtn.setBackground(new Color(50, 100, 160));
|
||||
printSpoolBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(printSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
printSpoolBtn.addActionListener(e -> printSpool());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(60, 60, 60));
|
||||
closeBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
bottomPanel.add(clearSpoolBtn);
|
||||
@@ -273,17 +251,13 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private JTextField createDarkField(String text, int cols) {
|
||||
private JTextField createField(String text, int cols) {
|
||||
JTextField tf = new JTextField(text, cols);
|
||||
tf.setBackground(DARK_FIELD_BG);
|
||||
tf.setForeground(DARK_FG);
|
||||
tf.setCaretColor(DARK_FG);
|
||||
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
tf.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(new Color(65, 65, 65)),
|
||||
new EmptyBorder(2, 4, 2, 4)));
|
||||
ThemeManager.styleTextField(tf);
|
||||
return tf;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ public class ScriptDialog extends JDialog {
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
mainPanel.setBackground(new Color(35, 35, 35));
|
||||
|
||||
Color fg = new Color(220, 220, 220);
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Header
|
||||
@@ -44,31 +42,24 @@ public class ScriptDialog extends JDialog {
|
||||
topPanel.setOpaque(false);
|
||||
JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>"
|
||||
+ "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>");
|
||||
descLabel.setForeground(new Color(180, 180, 180));
|
||||
descLabel.setFont(labelFont);
|
||||
topPanel.add(descLabel, BorderLayout.CENTER);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Script area
|
||||
scriptArea = new JTextArea();
|
||||
scriptArea.setBackground(new Color(25, 25, 25));
|
||||
scriptArea.setForeground(new Color(50, 205, 50));
|
||||
scriptArea.setCaretColor(Color.WHITE);
|
||||
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
scriptArea.setLineWrap(true);
|
||||
scriptArea.setWrapStyleWord(false);
|
||||
ThemeManager.styleTextArea(scriptArea);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(scriptArea);
|
||||
scrollPane.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(new Color(60, 60, 60)),
|
||||
new EmptyBorder(2, 2, 2, 2)));
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
|
||||
// Mnemonic helper buttons
|
||||
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
|
||||
tokenPanel.setOpaque(false);
|
||||
tokenPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
new LineBorder(new Color(60, 60, 60)), "Insert Keystroke Token"));
|
||||
((TitledBorder) tokenPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
tokenPanel.setBorder(ThemeManager.createTitledBorder("Insert Keystroke Token"));
|
||||
|
||||
String[] tokens = {
|
||||
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
|
||||
@@ -84,8 +75,7 @@ public class ScriptDialog extends JDialog {
|
||||
for (String token : tokens) {
|
||||
JButton btn = new JButton(token);
|
||||
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
|
||||
btn.setBackground(new Color(50, 50, 50));
|
||||
btn.setForeground(fg);
|
||||
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btn.setFocusable(false);
|
||||
btn.setMargin(new Insets(2, 4, 2, 4));
|
||||
btn.addActionListener(e -> insertToken(token));
|
||||
@@ -103,7 +93,7 @@ public class ScriptDialog extends JDialog {
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Ready");
|
||||
statusLabel.setForeground(new Color(160, 160, 160));
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
@@ -111,26 +101,22 @@ public class ScriptDialog extends JDialog {
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton loadBtn = new JButton("Load Script...");
|
||||
loadBtn.setBackground(new Color(50, 50, 50));
|
||||
loadBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
loadBtn.setFont(labelFont);
|
||||
loadBtn.addActionListener(e -> loadScript());
|
||||
|
||||
JButton saveBtn = new JButton("Save Script...");
|
||||
saveBtn.setBackground(new Color(50, 50, 50));
|
||||
saveBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveBtn.setFont(labelFont);
|
||||
saveBtn.addActionListener(e -> saveScript());
|
||||
|
||||
JButton runBtn = new JButton("Execute");
|
||||
runBtn.setBackground(new Color(50, 120, 50));
|
||||
runBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(runBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
runBtn.setFont(labelFont);
|
||||
runBtn.addActionListener(e -> executeScript());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(60, 60, 60));
|
||||
closeBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
@@ -144,6 +130,7 @@ public class ScriptDialog extends JDialog {
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private void insertToken(String token) {
|
||||
|
||||
@@ -19,16 +19,8 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
private final J3270App parentApp;
|
||||
|
||||
// Dark theme colors
|
||||
private static final Color DARK_BG = new Color(43, 43, 43);
|
||||
private static final Color DARK_BG_LIGHTER = new Color(55, 55, 55);
|
||||
private static final Color DARK_FG = new Color(224, 224, 224);
|
||||
private static final Color DARK_BORDER = new Color(70, 70, 70);
|
||||
private static final Color DARK_SELECTION = new Color(75, 110, 175);
|
||||
private static final Color DARK_BUTTON_BG = new Color(60, 63, 65);
|
||||
private static final Color DARK_FIELD_BG = new Color(50, 50, 50);
|
||||
|
||||
// Appearance tab
|
||||
private JComboBox<UITheme> uiThemeBox;
|
||||
private JComboBox<String> fontBox;
|
||||
private JSpinner fontSizeSpinner;
|
||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
|
||||
@@ -51,22 +43,30 @@ public class SettingsDialog extends JDialog {
|
||||
this.parentApp = parent;
|
||||
|
||||
initComponents();
|
||||
setSize(550, 450);
|
||||
setSize(560, 480);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
JTabbedPane tabbedPane = new JTabbedPane();
|
||||
ThemeManager.styleTabbedPane(tabbedPane);
|
||||
|
||||
tabbedPane.addTab("Appearance", createAppearancePanel());
|
||||
tabbedPane.addTab("Behavior", createBehaviorPanel());
|
||||
tabbedPane.addTab("Advanced", createAdvancedPanel());
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
|
||||
JButton btnExport = new JButton("Export Config...");
|
||||
JButton btnOk = new JButton("OK");
|
||||
ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
JButton btnApply = new JButton("Apply");
|
||||
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
JButton btnCancel = new JButton("Cancel");
|
||||
ThemeManager.styleButton(btnCancel, ThemeManager.ButtonVariant.CANCEL);
|
||||
|
||||
JButton btnOk = new JButton("OK");
|
||||
ThemeManager.styleButton(btnOk, ThemeManager.ButtonVariant.PRIMARY);
|
||||
|
||||
btnExport.addActionListener((ActionEvent e) -> {
|
||||
exportConfig();
|
||||
@@ -96,132 +96,11 @@ public class SettingsDialog extends JDialog {
|
||||
getContentPane().add(tabbedPane, BorderLayout.CENTER);
|
||||
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
// Apply dark theme to all components for cross-platform readability
|
||||
applyDarkTheme(getContentPane());
|
||||
applyDarkTheme(tabbedPane);
|
||||
applyDarkTheme(buttonPanel);
|
||||
getContentPane().setBackground(DARK_BG);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
// ========== Dark Theme Utility ==========
|
||||
|
||||
/**
|
||||
* Recursively apply dark theme to a component and all its children.
|
||||
* Ensures the Settings dialog is readable on Windows, Linux, and macOS.
|
||||
*/
|
||||
private void applyDarkTheme(Component comp) {
|
||||
if (comp instanceof JTabbedPane) {
|
||||
JTabbedPane tp = (JTabbedPane) comp;
|
||||
tp.setBackground(DARK_BG);
|
||||
tp.setForeground(DARK_FG);
|
||||
for (int i = 0; i < tp.getTabCount(); i++) {
|
||||
applyDarkTheme(tp.getComponentAt(i));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTable) {
|
||||
JTable table = (JTable) comp;
|
||||
table.setBackground(DARK_FIELD_BG);
|
||||
table.setForeground(DARK_FG);
|
||||
table.setSelectionBackground(DARK_SELECTION);
|
||||
table.setSelectionForeground(Color.WHITE);
|
||||
table.setGridColor(DARK_BORDER);
|
||||
JTableHeader header = table.getTableHeader();
|
||||
if (header != null) {
|
||||
header.setBackground(DARK_BG_LIGHTER);
|
||||
header.setForeground(DARK_FG);
|
||||
DefaultTableCellRenderer headerRenderer = new DefaultTableCellRenderer();
|
||||
headerRenderer.setBackground(DARK_BG_LIGHTER);
|
||||
headerRenderer.setForeground(DARK_FG);
|
||||
header.setDefaultRenderer(headerRenderer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JScrollPane) {
|
||||
JScrollPane sp = (JScrollPane) comp;
|
||||
sp.setBackground(DARK_BG);
|
||||
sp.getViewport().setBackground(DARK_FIELD_BG);
|
||||
applyDarkTheme(sp.getViewport().getView());
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JButton) {
|
||||
JButton btn = (JButton) comp;
|
||||
btn.setBackground(DARK_BUTTON_BG);
|
||||
btn.setForeground(DARK_FG);
|
||||
btn.setFocusPainted(false);
|
||||
btn.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(DARK_BORDER, 1),
|
||||
BorderFactory.createEmptyBorder(3, 10, 3, 10)));
|
||||
btn.setOpaque(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JComboBox) {
|
||||
JComboBox<?> cb = (JComboBox<?>) comp;
|
||||
cb.setBackground(DARK_FIELD_BG);
|
||||
cb.setForeground(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JSpinner) {
|
||||
JSpinner sp = (JSpinner) comp;
|
||||
sp.setBackground(DARK_FIELD_BG);
|
||||
sp.setForeground(DARK_FG);
|
||||
JComponent editor = sp.getEditor();
|
||||
if (editor instanceof JSpinner.DefaultEditor) {
|
||||
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
|
||||
tf.setBackground(DARK_FIELD_BG);
|
||||
tf.setForeground(DARK_FG);
|
||||
tf.setCaretColor(DARK_FG);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTextField) {
|
||||
JTextField tf = (JTextField) comp;
|
||||
tf.setBackground(DARK_FIELD_BG);
|
||||
tf.setForeground(DARK_FG);
|
||||
tf.setCaretColor(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JCheckBox) {
|
||||
JCheckBox cb = (JCheckBox) comp;
|
||||
cb.setBackground(DARK_BG);
|
||||
cb.setForeground(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JLabel) {
|
||||
comp.setForeground(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic panel / container
|
||||
// Skip color swatch panels — their background IS the color
|
||||
if (comp instanceof JPanel && "colorSwatch".equals(comp.getName())) {
|
||||
return;
|
||||
}
|
||||
comp.setBackground(DARK_BG);
|
||||
comp.setForeground(DARK_FG);
|
||||
|
||||
if (comp instanceof JPanel) {
|
||||
JPanel panel = (JPanel) comp;
|
||||
// Style titled borders
|
||||
if (panel.getBorder() instanceof TitledBorder) {
|
||||
TitledBorder tb = (TitledBorder) panel.getBorder();
|
||||
tb.setTitleColor(DARK_FG);
|
||||
}
|
||||
}
|
||||
|
||||
if (comp instanceof Container) {
|
||||
for (Component child : ((Container) comp).getComponents()) {
|
||||
applyDarkTheme(child);
|
||||
}
|
||||
}
|
||||
ThemeManager.applyTheme(comp);
|
||||
}
|
||||
|
||||
// ========== Tab Panels ==========
|
||||
@@ -232,9 +111,22 @@ public class SettingsDialog extends JDialog {
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
// Font family
|
||||
// UI Theme
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel themeLabel = new JLabel("Java UI Theme:");
|
||||
panel.add(themeLabel, gbc);
|
||||
|
||||
uiThemeBox = new JComboBox<>(UITheme.values());
|
||||
uiThemeBox.setSelectedItem(Settings.getJavaUiTheme());
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(uiThemeBox, gbc);
|
||||
|
||||
// Font family
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel fontLabel = new JLabel("Terminal Font:");
|
||||
panel.add(fontLabel, gbc);
|
||||
|
||||
@@ -254,7 +146,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Font size
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.gridy = 2;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel sizeLabel = new JLabel("Font Size:");
|
||||
panel.add(sizeLabel, gbc);
|
||||
@@ -267,7 +159,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.gridy = 3;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel graphicsLabel = new JLabel("Graphics Mode:");
|
||||
panel.add(graphicsLabel, gbc);
|
||||
@@ -279,7 +171,7 @@ public class SettingsDialog extends JDialog {
|
||||
panel.add(graphicsModeBox, gbc);
|
||||
|
||||
// Fill remaining space
|
||||
gbc.gridy = 3;
|
||||
gbc.gridy = 4;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
@@ -429,6 +321,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton btnResetColors = new JButton("Reset to Defaults");
|
||||
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnResetColors.addActionListener(e -> {
|
||||
for (int i=0; i<16; i++) {
|
||||
tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i];
|
||||
@@ -436,8 +329,6 @@ public class SettingsDialog extends JDialog {
|
||||
for (int i=0; i<monoKeys.length; i++) {
|
||||
tempMonoColors.put(monoKeys[i], monoDefs[i]);
|
||||
}
|
||||
// Repaint container implicitly handled if we trigger a UI update,
|
||||
// but for simplicity user can just close/reopen or have it refresh on save
|
||||
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
|
||||
});
|
||||
resetPanel.add(btnResetColors);
|
||||
@@ -498,6 +389,7 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
|
||||
JTable table = new JTable(keymapModel);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setFillsViewportHeight(true);
|
||||
|
||||
@@ -510,9 +402,8 @@ public class SettingsDialog extends JDialog {
|
||||
captureDialog.setSize(320, 100);
|
||||
captureDialog.setLocationRelativeTo(this);
|
||||
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
|
||||
lbl.setForeground(DARK_FG);
|
||||
captureDialog.getContentPane().setBackground(DARK_BG);
|
||||
captureDialog.add(lbl);
|
||||
ThemeManager.applyThemeToWindow(captureDialog);
|
||||
captureDialog.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
@@ -532,6 +423,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Replace Binding — sets the action to exactly one new key
|
||||
JButton btnReplace = new JButton("Replace Binding");
|
||||
ThemeManager.styleButton(btnReplace, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnReplace.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -545,6 +437,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Add Binding — appends an additional key to the existing binding(s)
|
||||
JButton btnAdd = new JButton("Add Binding");
|
||||
ThemeManager.styleButton(btnAdd, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnAdd.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -565,6 +458,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Remove Last — removes the last comma-separated binding entry
|
||||
JButton btnRemoveLast = new JButton("Remove Last");
|
||||
ThemeManager.styleButton(btnRemoveLast, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnRemoveLast.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -585,6 +479,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Unbind All — clears all bindings for the action
|
||||
JButton btnUnbind = new JButton("Unbind All");
|
||||
ThemeManager.styleButton(btnUnbind, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnUnbind.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -596,6 +491,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Reset Keymaps — restore all defaults
|
||||
JButton btnReset = new JButton("Reset All");
|
||||
ThemeManager.styleButton(btnReset, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnReset.addActionListener(e -> {
|
||||
for (int row = 0; row < keymapModel.getRowCount(); row++) {
|
||||
String action = (String) keymapModel.getValueAt(row, 0);
|
||||
@@ -628,6 +524,12 @@ public class SettingsDialog extends JDialog {
|
||||
private boolean applySettings() {
|
||||
try {
|
||||
// Apply Appearance
|
||||
UITheme selectedTheme = (UITheme) uiThemeBox.getSelectedItem();
|
||||
if (selectedTheme != null) {
|
||||
Settings.setJavaUiTheme(selectedTheme);
|
||||
ThemeManager.setTheme(selectedTheme);
|
||||
}
|
||||
|
||||
String fontFam = (String) fontBox.getSelectedItem();
|
||||
if (fontFam != null) {
|
||||
Settings.setFontFamily(fontFam);
|
||||
@@ -668,6 +570,7 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
|
||||
parentApp.getTerminalPanel().reloadSettings();
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -26,29 +26,22 @@ public class StatusBar extends JPanel {
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
|
||||
// OIA colors
|
||||
private static final Color OIA_BG = new Color(20, 20, 20);
|
||||
private static final Color OIA_FG = new Color(50, 205, 50);
|
||||
private static final Color OIA_DIM = new Color(80, 80, 80);
|
||||
private static final Color OIA_ALERT = new Color(255, 80, 80);
|
||||
private static final Color OIA_WARN = new Color(255, 200, 80);
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
setBackground(OIA_BG);
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
|
||||
setBackground(ThemeManager.getStatusBarBg());
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
|
||||
setPreferredSize(new Dimension(800, 22));
|
||||
|
||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
|
||||
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
||||
luName = createLabel("", oiaFont, OIA_FG);
|
||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
|
||||
codePageInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG);
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
|
||||
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
lockStatus = createLabel("", oiaFont, ThemeManager.getOiaFgAlert());
|
||||
fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
|
||||
codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
|
||||
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
|
||||
cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
|
||||
add(Box.createHorizontalStrut(6));
|
||||
add(connectionStatus);
|
||||
@@ -82,10 +75,17 @@ public class StatusBar extends JPanel {
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
public void applyTheme(UITheme theme) {
|
||||
setBackground(ThemeManager.getStatusBarBg(theme));
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
UITheme theme = ThemeManager.getTheme();
|
||||
if (client == null) {
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
tlsStatus.setText("");
|
||||
luName.setText("");
|
||||
lockStatus.setText("");
|
||||
@@ -101,38 +101,38 @@ public class StatusBar extends JPanel {
|
||||
switch (state) {
|
||||
case NOT_CONNECTED:
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
break;
|
||||
case TCP_PENDING:
|
||||
case TELNET_PENDING:
|
||||
connectionStatus.setText("Connecting...");
|
||||
connectionStatus.setForeground(OIA_ALERT);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||
break;
|
||||
case CONNECTED_3270:
|
||||
connectionStatus.setText("TN3270");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_TN3270E:
|
||||
connectionStatus.setText("TN3270E");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_SSCP:
|
||||
connectionStatus.setText("SSCP-LU");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_NVT:
|
||||
case CONNECTED_NVT_CHAR:
|
||||
case CONNECTED_E_NVT:
|
||||
connectionStatus.setText("NVT");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_UNBOUND:
|
||||
connectionStatus.setText("Unbound");
|
||||
connectionStatus.setForeground(OIA_WARN);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
break;
|
||||
default:
|
||||
connectionStatus.setText(state.name());
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -144,11 +144,11 @@ public class StatusBar extends JPanel {
|
||||
String protocol = session != null ? session.getProtocol() : "TLS";
|
||||
if (verified) {
|
||||
tlsStatus.setText("🔒 TLS");
|
||||
tlsStatus.setForeground(OIA_FG);
|
||||
tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
|
||||
} else {
|
||||
tlsStatus.setText("🔓 TLS (Unverified)");
|
||||
tlsStatus.setForeground(new Color(255, 180, 80));
|
||||
tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
|
||||
}
|
||||
} else {
|
||||
@@ -164,6 +164,7 @@ public class StatusBar extends JPanel {
|
||||
lu = "LU:" + client.getConfig().getLuName();
|
||||
}
|
||||
luName.setText(lu);
|
||||
luName.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
|
||||
// Lock / Inhibit status
|
||||
int inhibit = client.getOIA().getInputInhibited();
|
||||
@@ -191,10 +192,10 @@ public class StatusBar extends JPanel {
|
||||
lockStatus.setText("X LOCKED");
|
||||
break;
|
||||
}
|
||||
lockStatus.setForeground(OIA_ALERT);
|
||||
lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||
} else if (client.getInputProcessor().isInsertMode()) {
|
||||
lockStatus.setText("INSERT");
|
||||
lockStatus.setForeground(OIA_FG);
|
||||
lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
} else {
|
||||
lockStatus.setText("");
|
||||
}
|
||||
@@ -203,10 +204,10 @@ public class StatusBar extends JPanel {
|
||||
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
|
||||
if (client.getOIA().isNumeric()) {
|
||||
fieldTypeStatus.setText("NUM");
|
||||
fieldTypeStatus.setForeground(OIA_WARN);
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
} else {
|
||||
fieldTypeStatus.setText("ALPHA");
|
||||
fieldTypeStatus.setForeground(OIA_DIM);
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
}
|
||||
} else {
|
||||
fieldTypeStatus.setText("");
|
||||
@@ -215,6 +216,7 @@ public class StatusBar extends JPanel {
|
||||
// Active Code Page
|
||||
String cp = client.getCodePage();
|
||||
codePageInfo.setText(cp != null ? "CP" + cp : "");
|
||||
codePageInfo.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
|
||||
|
||||
// Model & Dimensions info
|
||||
@@ -222,11 +224,13 @@ public class StatusBar extends JPanel {
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
|
||||
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
|
||||
// Cursor position and buffer address
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int row = sb.getCursorRow() + 1;
|
||||
int col = sb.getCursorCol() + 1;
|
||||
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,6 +434,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
public void pasteClipboard() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) return;
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
.getData(DataFlavor.stringFlavor);
|
||||
if (text != null && !text.isEmpty()) {
|
||||
client.sendNVTString(text);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
.getData(DataFlavor.stringFlavor);
|
||||
@@ -481,24 +491,29 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void showContextMenu(MouseEvent e) {
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
ThemeManager.stylePopupMenu(popup);
|
||||
|
||||
JMenuItem copyItem = new JMenuItem("Copy");
|
||||
ThemeManager.styleMenuItem(copyItem);
|
||||
copyItem.setEnabled(hasSelection());
|
||||
copyItem.addActionListener(ev -> copySelection());
|
||||
popup.add(copyItem);
|
||||
|
||||
JMenuItem pasteItem = new JMenuItem("Paste");
|
||||
ThemeManager.styleMenuItem(pasteItem);
|
||||
pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession());
|
||||
pasteItem.addActionListener(ev -> pasteClipboard());
|
||||
popup.add(pasteItem);
|
||||
|
||||
JMenuItem selectAllItem = new JMenuItem("Select All");
|
||||
ThemeManager.styleMenuItem(selectAllItem);
|
||||
selectAllItem.addActionListener(ev -> selectAll());
|
||||
popup.add(selectAllItem);
|
||||
|
||||
popup.addSeparator();
|
||||
|
||||
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||
ThemeManager.styleMenuItem(blockModeItem);
|
||||
blockModeItem.addActionListener(ev -> {
|
||||
blockSelectMode = blockModeItem.isSelected();
|
||||
haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
|
||||
@@ -693,19 +708,63 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
@Override
|
||||
protected void processKeyEvent(KeyEvent e) {
|
||||
if (e.getID() == KeyEvent.KEY_TYPED) {
|
||||
char ch = e.getKeyChar();
|
||||
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
||||
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||
if (client != null) {
|
||||
ConnectionState state = client.getConnectionState();
|
||||
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
|
||||
if (client != null) {
|
||||
ConnectionState state = client.getConnectionState();
|
||||
if (state.isNvt()) {
|
||||
if (e.getID() == KeyEvent.KEY_TYPED) {
|
||||
char ch = e.getKeyChar();
|
||||
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
||||
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||
try {
|
||||
client.sendNVTChar(ch);
|
||||
} catch (Exception ignored) {}
|
||||
e.consume();
|
||||
return;
|
||||
} else if (state.isFullSession()) {
|
||||
} else if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||
if (ch > 0 && ch < 0x20) {
|
||||
try {
|
||||
client.sendNVTChar(ch);
|
||||
} catch (Exception ignored) {}
|
||||
e.consume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (e.getID() == KeyEvent.KEY_PRESSED) {
|
||||
if (e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||
int code = e.getKeyCode();
|
||||
if (code >= KeyEvent.VK_A && code <= KeyEvent.VK_Z) {
|
||||
char ctrlChar = (char) (code - KeyEvent.VK_A + 1);
|
||||
try {
|
||||
client.sendNVTChar(ctrlChar);
|
||||
} catch (Exception ignored) {}
|
||||
e.consume();
|
||||
return;
|
||||
} else if (code == KeyEvent.VK_OPEN_BRACKET) { // Ctrl+[ = ESC
|
||||
try {
|
||||
client.sendNVTChar('\u001B');
|
||||
} catch (Exception ignored) {}
|
||||
e.consume();
|
||||
return;
|
||||
} else if (code == KeyEvent.VK_BACK_SLASH) { // Ctrl+\ = FS
|
||||
try {
|
||||
client.sendNVTChar('\u001C');
|
||||
} catch (Exception ignored) {}
|
||||
e.consume();
|
||||
return;
|
||||
} else if (code == KeyEvent.VK_CLOSE_BRACKET) { // Ctrl+] = GS
|
||||
try {
|
||||
client.sendNVTChar('\u001D');
|
||||
} catch (Exception ignored) {}
|
||||
e.consume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e.getID() == KeyEvent.KEY_TYPED) {
|
||||
char ch = e.getKeyChar();
|
||||
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
||||
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||
if (state.isFullSession()) {
|
||||
client.typeCharacter(ch);
|
||||
refreshScreen();
|
||||
e.consume();
|
||||
@@ -731,7 +790,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private void handleEnter() {
|
||||
if (client != null) {
|
||||
ConnectionState state = client.getConnectionState();
|
||||
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
|
||||
if (state.isNvt()) {
|
||||
try {
|
||||
client.sendNVTString("\r\n");
|
||||
} catch (Exception ignored) {}
|
||||
@@ -744,6 +803,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void handleReset() {
|
||||
if (client != null) {
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
client.sendNVTChar('\u001B');
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
clearSearchHighlight();
|
||||
clearSelection();
|
||||
client.reset();
|
||||
@@ -753,6 +818,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void handleTab(boolean shift) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
if (shift) {
|
||||
client.sendNVTString("\u001B[Z");
|
||||
} else {
|
||||
client.sendNVTChar('\t');
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
if (shift) client.backTab();
|
||||
else client.tab();
|
||||
refreshScreen();
|
||||
@@ -761,6 +836,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void handleCursor(String dir) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
switch (dir) {
|
||||
case "up": client.sendNVTString("\u001B[A"); break;
|
||||
case "down": client.sendNVTString("\u001B[B"); break;
|
||||
case "left": client.sendNVTString("\u001B[D"); break;
|
||||
case "right": client.sendNVTString("\u001B[C"); break;
|
||||
case "home": client.sendNVTString("\u001B[H"); break;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
switch (dir) {
|
||||
case "up": client.cursorUp(); break;
|
||||
case "down": client.cursorDown(); break;
|
||||
@@ -774,11 +861,35 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void handlePF(int n) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
client.sendNVTString(getNvtFunctionKeySequence(n));
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
client.sendPF(n);
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private String getNvtFunctionKeySequence(int n) {
|
||||
switch (n) {
|
||||
case 1: return "\u001BOP";
|
||||
case 2: return "\u001BOQ";
|
||||
case 3: return "\u001BOR";
|
||||
case 4: return "\u001BOS";
|
||||
case 5: return "\u001B[15~";
|
||||
case 6: return "\u001B[17~";
|
||||
case 7: return "\u001B[18~";
|
||||
case 8: return "\u001B[19~";
|
||||
case 9: return "\u001B[20~";
|
||||
case 10: return "\u001B[21~";
|
||||
case 11: return "\u001B[23~";
|
||||
case 12: return "\u001B[24~";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePA(int n) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendPA(n);
|
||||
@@ -788,6 +899,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void handleEraseEOF() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
client.sendNVTString("\u001B[F");
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
client.getInputProcessor().eraseEof();
|
||||
refreshScreen();
|
||||
}
|
||||
@@ -795,6 +912,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void handleDelete() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
client.sendNVTString("\u001B[3~");
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
client.getInputProcessor().deleteChar();
|
||||
refreshScreen();
|
||||
}
|
||||
@@ -803,7 +926,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private void handleBackspace() {
|
||||
if (client != null) {
|
||||
ConnectionState state = client.getConnectionState();
|
||||
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
|
||||
if (state.isNvt()) {
|
||||
try {
|
||||
client.sendNVTChar('\b');
|
||||
} catch (Exception ignored) {}
|
||||
@@ -824,6 +947,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
private void handleClear() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (client.getConnectionState().isNvt()) {
|
||||
try {
|
||||
client.sendNVTChar('\u000C');
|
||||
} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
client.sendClear();
|
||||
refreshScreen();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,955 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.border.CompoundBorder;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import javax.swing.plaf.basic.BasicButtonUI;
|
||||
import javax.swing.plaf.basic.BasicMenuBarUI;
|
||||
import javax.swing.plaf.basic.BasicMenuItemUI;
|
||||
import javax.swing.plaf.basic.BasicMenuUI;
|
||||
import javax.swing.plaf.basic.BasicPopupMenuUI;
|
||||
import javax.swing.plaf.basic.BasicTabbedPaneUI;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Centralized theme manager for j3270 Java desktop UI.
|
||||
* Provides high-contrast, platform-independent Light and Dark theme palettes,
|
||||
* custom Swing UI delegates, and recursive styling helpers for all dialogs and menus.
|
||||
*/
|
||||
public final class ThemeManager {
|
||||
|
||||
private static UITheme currentTheme = UITheme.DARK;
|
||||
private static final List<Consumer<UITheme>> themeChangeListeners = new ArrayList<>();
|
||||
|
||||
private ThemeManager() {}
|
||||
|
||||
public static UITheme getTheme() {
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
public static boolean isDark() {
|
||||
return currentTheme == UITheme.DARK;
|
||||
}
|
||||
|
||||
public static boolean isLight() {
|
||||
return currentTheme == UITheme.LIGHT;
|
||||
}
|
||||
|
||||
public static void setTheme(UITheme theme) {
|
||||
if (theme == null) theme = UITheme.DARK;
|
||||
if (currentTheme != theme) {
|
||||
currentTheme = theme;
|
||||
applyUIManagerDefaults(theme);
|
||||
for (Consumer<UITheme> listener : new ArrayList<>(themeChangeListeners)) {
|
||||
try {
|
||||
listener.accept(theme);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void addThemeChangeListener(Consumer<UITheme> listener) {
|
||||
if (listener != null && !themeChangeListeners.contains(listener)) {
|
||||
themeChangeListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeThemeChangeListener(Consumer<UITheme> listener) {
|
||||
themeChangeListeners.remove(listener);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Color Palette Tokens
|
||||
// =========================================================================
|
||||
|
||||
public static Color getBgMain() { return getBgMain(currentTheme); }
|
||||
public static Color getBgMain(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(28, 28, 28) : new Color(242, 242, 244);
|
||||
}
|
||||
|
||||
public static Color getBgPanel() { return getBgPanel(currentTheme); }
|
||||
public static Color getBgPanel(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(36, 36, 36) : new Color(250, 250, 252);
|
||||
}
|
||||
|
||||
public static Color getBgComponent() { return getBgComponent(currentTheme); }
|
||||
public static Color getBgComponent(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(46, 46, 46) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getBgComponentLighter() { return getBgComponentLighter(currentTheme); }
|
||||
public static Color getBgComponentLighter(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(56, 56, 56) : new Color(236, 236, 238);
|
||||
}
|
||||
|
||||
public static Color getFgMain() { return getFgMain(currentTheme); }
|
||||
public static Color getFgMain(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(238, 238, 238) : new Color(24, 24, 24);
|
||||
}
|
||||
|
||||
public static Color getFgMuted() { return getFgMuted(currentTheme); }
|
||||
public static Color getFgMuted(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(175, 175, 175) : new Color(90, 90, 90);
|
||||
}
|
||||
|
||||
public static Color getFgDisabled() { return getFgDisabled(currentTheme); }
|
||||
public static Color getFgDisabled(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(160, 160, 160);
|
||||
}
|
||||
|
||||
public static Color getBorder() { return getBorder(currentTheme); }
|
||||
public static Color getBorder(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(65, 65, 65) : new Color(205, 205, 205);
|
||||
}
|
||||
public static Color getBorderColor() { return getBorder(currentTheme); }
|
||||
public static Color getBorderColor(UITheme t) { return getBorder(t); }
|
||||
|
||||
public static Color getBorderFocused() { return getBorderFocused(currentTheme); }
|
||||
public static Color getBorderFocused(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(80, 140, 230) : new Color(50, 115, 220);
|
||||
}
|
||||
|
||||
public static Color getSelectionBg() { return getSelectionBg(currentTheme); }
|
||||
public static Color getSelectionBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(55, 105, 180) : new Color(60, 125, 215);
|
||||
}
|
||||
|
||||
public static Color getSelectionFg() { return getSelectionFg(currentTheme); }
|
||||
public static Color getSelectionFg(UITheme t) {
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getMenuBarBg() { return getMenuBarBg(currentTheme); }
|
||||
public static Color getMenuBarBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(26, 26, 26) : new Color(240, 240, 242);
|
||||
}
|
||||
|
||||
public static Color getMenuBarFg() { return getMenuBarFg(currentTheme); }
|
||||
public static Color getMenuBarFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(25, 25, 25);
|
||||
}
|
||||
|
||||
public static Color getMenuPopupBg() { return getMenuPopupBg(currentTheme); }
|
||||
public static Color getMenuPopupBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getMenuItemFg() { return getMenuItemFg(currentTheme); }
|
||||
public static Color getMenuItemFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(230, 230, 230) : new Color(30, 30, 30);
|
||||
}
|
||||
|
||||
public static Color getMenuItemHoverBg() { return getMenuItemHoverBg(currentTheme); }
|
||||
public static Color getMenuItemHoverBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(55, 95, 160) : new Color(220, 232, 250);
|
||||
}
|
||||
|
||||
public static Color getMenuItemHoverFg() { return getMenuItemHoverFg(currentTheme); }
|
||||
public static Color getMenuItemHoverFg(UITheme t) {
|
||||
return t == UITheme.DARK ? Color.WHITE : new Color(15, 15, 15);
|
||||
}
|
||||
|
||||
public static Color getTabBg() { return getTabBg(currentTheme); }
|
||||
public static Color getTabBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(34, 34, 34) : new Color(230, 230, 232);
|
||||
}
|
||||
|
||||
public static Color getTabFg() { return getTabFg(currentTheme); }
|
||||
public static Color getTabFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(185, 185, 185) : new Color(75, 75, 75);
|
||||
}
|
||||
|
||||
public static Color getTabSelectedBg() { return getTabSelectedBg(currentTheme); }
|
||||
public static Color getTabSelectedBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(50, 50, 50) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getTabSelectedFg() { return getTabSelectedFg(currentTheme); }
|
||||
public static Color getTabSelectedFg(UITheme t) {
|
||||
return t == UITheme.DARK ? Color.WHITE : new Color(20, 20, 20);
|
||||
}
|
||||
|
||||
public static Color getTableHeaderBg() { return getTableHeaderBg(currentTheme); }
|
||||
public static Color getTableHeaderBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(46, 46, 46) : new Color(232, 232, 236);
|
||||
}
|
||||
|
||||
public static Color getTableHeaderFg() { return getTableHeaderFg(currentTheme); }
|
||||
public static Color getTableHeaderFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
|
||||
}
|
||||
|
||||
public static Color getTableRowEven() { return getTableRowEven(currentTheme); }
|
||||
public static Color getTableRowEven(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getTableRowOdd() { return getTableRowOdd(currentTheme); }
|
||||
public static Color getTableRowOdd(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(33, 33, 33) : new Color(246, 247, 249);
|
||||
}
|
||||
|
||||
public static Color getTableGrid() { return getTableGrid(currentTheme); }
|
||||
public static Color getTableGrid(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(55, 55, 55) : new Color(225, 225, 225);
|
||||
}
|
||||
|
||||
public static Color getCodeAreaBg() { return getCodeAreaBg(currentTheme); }
|
||||
public static Color getCodeAreaBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(22, 22, 22) : new Color(252, 252, 252);
|
||||
}
|
||||
|
||||
public static Color getCodeAreaFg() { return getCodeAreaFg(currentTheme); }
|
||||
public static Color getCodeAreaFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(80, 230, 80) : new Color(0, 120, 0);
|
||||
}
|
||||
|
||||
public static Color getStatusBarBg() { return getStatusBarBg(currentTheme); }
|
||||
public static Color getStatusBarBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(20, 20, 20) : new Color(235, 235, 238);
|
||||
}
|
||||
|
||||
public static Color getStatusBarBorder() { return getStatusBarBorder(currentTheme); }
|
||||
public static Color getStatusBarBorder(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(42, 42, 42) : new Color(210, 210, 215);
|
||||
}
|
||||
|
||||
public static Color getOiaFgNormal() { return getOiaFgNormal(currentTheme); }
|
||||
public static Color getOiaFgNormal(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(50, 205, 50) : new Color(0, 130, 0);
|
||||
}
|
||||
|
||||
public static Color getOiaFgDim() { return getOiaFgDim(currentTheme); }
|
||||
public static Color getOiaFgDim(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(90, 90, 90);
|
||||
}
|
||||
|
||||
public static Color getOiaFgAlert() { return getOiaFgAlert(currentTheme); }
|
||||
public static Color getOiaFgAlert(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(255, 80, 80) : new Color(190, 20, 20);
|
||||
}
|
||||
|
||||
public static Color getOiaFgWarn() { return getOiaFgWarn(currentTheme); }
|
||||
public static Color getOiaFgWarn(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Button Variants & Color helpers
|
||||
// =========================================================================
|
||||
|
||||
public enum ButtonVariant {
|
||||
DEFAULT,
|
||||
PRIMARY,
|
||||
CANCEL,
|
||||
DANGER,
|
||||
ACCENT
|
||||
}
|
||||
|
||||
public static Color getButtonBg(ButtonVariant variant) {
|
||||
return getButtonBg(variant, currentTheme, false, false);
|
||||
}
|
||||
|
||||
public static Color getButtonBg(ButtonVariant variant, UITheme t) {
|
||||
return getButtonBg(variant, t, false, false);
|
||||
}
|
||||
|
||||
public static Color getButtonBg(ButtonVariant variant, UITheme t, boolean hover, boolean pressed) {
|
||||
switch (variant) {
|
||||
case PRIMARY:
|
||||
if (pressed) return new Color(25, 105, 35);
|
||||
if (hover) return new Color(38, 145, 48);
|
||||
return new Color(32, 128, 42);
|
||||
|
||||
case DANGER:
|
||||
if (pressed) return new Color(125, 30, 30);
|
||||
if (hover) return new Color(165, 45, 45);
|
||||
return new Color(145, 38, 38);
|
||||
|
||||
case ACCENT:
|
||||
if (pressed) return new Color(35, 80, 145);
|
||||
if (hover) return new Color(55, 115, 195);
|
||||
return new Color(45, 100, 175);
|
||||
|
||||
case CANCEL:
|
||||
case DEFAULT:
|
||||
default:
|
||||
if (t == UITheme.DARK) {
|
||||
if (pressed) return new Color(42, 42, 42);
|
||||
if (hover) return new Color(68, 68, 68);
|
||||
return new Color(56, 56, 56);
|
||||
} else {
|
||||
if (pressed) return new Color(210, 210, 212);
|
||||
if (hover) return new Color(228, 228, 232);
|
||||
return new Color(238, 238, 240);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Color getButtonFg(ButtonVariant variant, UITheme t) {
|
||||
switch (variant) {
|
||||
case PRIMARY:
|
||||
case DANGER:
|
||||
case ACCENT:
|
||||
return Color.WHITE;
|
||||
case CANCEL:
|
||||
case DEFAULT:
|
||||
default:
|
||||
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// UIManager Defaults Configuration
|
||||
// =========================================================================
|
||||
|
||||
public static void applyUIManagerDefaults(UITheme theme) {
|
||||
Color bgMain = getBgMain(theme);
|
||||
Color bgPanel = getBgPanel(theme);
|
||||
Color bgComp = getBgComponent(theme);
|
||||
Color fgMain = getFgMain(theme);
|
||||
Color fgMuted = getFgMuted(theme);
|
||||
Color border = getBorder(theme);
|
||||
Color selBg = getSelectionBg(theme);
|
||||
Color selFg = getSelectionFg(theme);
|
||||
|
||||
UIManager.put("Panel.background", bgPanel);
|
||||
UIManager.put("Panel.foreground", fgMain);
|
||||
UIManager.put("Label.foreground", fgMain);
|
||||
|
||||
UIManager.put("TextField.background", bgComp);
|
||||
UIManager.put("TextField.foreground", fgMain);
|
||||
UIManager.put("TextField.caretForeground", fgMain);
|
||||
UIManager.put("TextField.selectionBackground", selBg);
|
||||
UIManager.put("TextField.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("TextArea.background", bgComp);
|
||||
UIManager.put("TextArea.foreground", fgMain);
|
||||
UIManager.put("TextArea.caretForeground", fgMain);
|
||||
UIManager.put("TextArea.selectionBackground", selBg);
|
||||
UIManager.put("TextArea.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("Button.background", getBgComponentLighter(theme));
|
||||
UIManager.put("Button.foreground", fgMain);
|
||||
UIManager.put("Button.select", selBg);
|
||||
|
||||
UIManager.put("ComboBox.background", bgComp);
|
||||
UIManager.put("ComboBox.foreground", fgMain);
|
||||
UIManager.put("ComboBox.selectionBackground", selBg);
|
||||
UIManager.put("ComboBox.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("Table.background", bgComp);
|
||||
UIManager.put("Table.foreground", fgMain);
|
||||
UIManager.put("Table.selectionBackground", selBg);
|
||||
UIManager.put("Table.selectionForeground", selFg);
|
||||
UIManager.put("Table.gridColor", getTableGrid(theme));
|
||||
|
||||
UIManager.put("TableHeader.background", getTableHeaderBg(theme));
|
||||
UIManager.put("TableHeader.foreground", getTableHeaderFg(theme));
|
||||
|
||||
UIManager.put("ScrollPane.background", bgPanel);
|
||||
UIManager.put("Viewport.background", bgComp);
|
||||
|
||||
UIManager.put("MenuBar.background", getMenuBarBg(theme));
|
||||
UIManager.put("MenuBar.foreground", getMenuBarFg(theme));
|
||||
UIManager.put("Menu.background", getMenuBarBg(theme));
|
||||
UIManager.put("Menu.foreground", getMenuBarFg(theme));
|
||||
UIManager.put("Menu.selectionBackground", selBg);
|
||||
UIManager.put("Menu.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("PopupMenu.background", getMenuPopupBg(theme));
|
||||
UIManager.put("PopupMenu.foreground", getMenuItemFg(theme));
|
||||
|
||||
UIManager.put("MenuItem.background", getMenuPopupBg(theme));
|
||||
UIManager.put("MenuItem.foreground", getMenuItemFg(theme));
|
||||
UIManager.put("MenuItem.selectionBackground", selBg);
|
||||
UIManager.put("MenuItem.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("CheckBoxMenuItem.background", getMenuPopupBg(theme));
|
||||
UIManager.put("CheckBoxMenuItem.foreground", getMenuItemFg(theme));
|
||||
UIManager.put("CheckBoxMenuItem.selectionBackground", selBg);
|
||||
UIManager.put("CheckBoxMenuItem.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("RadioButtonMenuItem.background", getMenuPopupBg(theme));
|
||||
UIManager.put("RadioButtonMenuItem.foreground", getMenuItemFg(theme));
|
||||
UIManager.put("RadioButtonMenuItem.selectionBackground", selBg);
|
||||
UIManager.put("RadioButtonMenuItem.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("Separator.background", border);
|
||||
UIManager.put("Separator.foreground", border);
|
||||
|
||||
UIManager.put("TabbedPane.background", getTabBg(theme));
|
||||
UIManager.put("TabbedPane.foreground", getTabFg(theme));
|
||||
UIManager.put("TabbedPane.selected", getTabSelectedBg(theme));
|
||||
UIManager.put("TabbedPane.selectHighlight", getTabSelectedBg(theme));
|
||||
|
||||
UIManager.put("CheckBox.background", bgPanel);
|
||||
UIManager.put("CheckBox.foreground", fgMain);
|
||||
UIManager.put("RadioButton.background", bgPanel);
|
||||
UIManager.put("RadioButton.foreground", fgMain);
|
||||
|
||||
UIManager.put("TitledBorder.titleColor", fgMain);
|
||||
UIManager.put("OptionPane.background", bgPanel);
|
||||
UIManager.put("OptionPane.messageForeground", fgMain);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Component Styling & Custom UI Delegates
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Styles a button with custom rendering, ensuring consistent high contrast
|
||||
* and proper background color across Windows, macOS, and Linux.
|
||||
*/
|
||||
public static JButton styleButton(JButton button, ButtonVariant variant) {
|
||||
if (button == null) return null;
|
||||
button.setUI(new StyledButtonUI(variant));
|
||||
button.setFocusPainted(false);
|
||||
button.setOpaque(false);
|
||||
button.setContentAreaFilled(false);
|
||||
button.setBorder(BorderFactory.createEmptyBorder(6, 14, 6, 14));
|
||||
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
return button;
|
||||
}
|
||||
|
||||
public static JButton createStyledButton(String text, ButtonVariant variant, Runnable action) {
|
||||
JButton btn = new JButton(text);
|
||||
styleButton(btn, variant);
|
||||
if (action != null) {
|
||||
btn.addActionListener(e -> action.run());
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
public static JTextField styleTextField(JTextField field) {
|
||||
if (field == null) return null;
|
||||
field.setBackground(getBgComponent());
|
||||
field.setForeground(getFgMain());
|
||||
field.setCaretColor(getFgMain());
|
||||
field.setSelectionColor(getSelectionBg());
|
||||
field.setSelectedTextColor(getSelectionFg());
|
||||
field.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(getBorder(), 1, true),
|
||||
new EmptyBorder(4, 8, 4, 8)));
|
||||
return field;
|
||||
}
|
||||
|
||||
public static JTextArea styleTextArea(JTextArea area) {
|
||||
if (area == null) return null;
|
||||
area.setBackground(getCodeAreaBg());
|
||||
area.setForeground(getCodeAreaFg());
|
||||
area.setCaretColor(getFgMain());
|
||||
area.setSelectionColor(getSelectionBg());
|
||||
area.setSelectedTextColor(getSelectionFg());
|
||||
return area;
|
||||
}
|
||||
|
||||
public static JComboBox<?> styleComboBox(JComboBox<?> box) {
|
||||
if (box == null) return null;
|
||||
box.setBackground(getBgComponent());
|
||||
box.setForeground(getFgMain());
|
||||
box.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
setBorder(new EmptyBorder(4, 8, 4, 8));
|
||||
if (isSelected) {
|
||||
setBackground(getSelectionBg());
|
||||
setForeground(getSelectionFg());
|
||||
} else {
|
||||
setBackground(getBgComponent());
|
||||
setForeground(getFgMain());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
public static JTable styleTable(JTable table) {
|
||||
if (table == null) return null;
|
||||
table.setBackground(getBgComponent());
|
||||
table.setForeground(getFgMain());
|
||||
table.setSelectionBackground(getSelectionBg());
|
||||
table.setSelectionForeground(getSelectionFg());
|
||||
table.setGridColor(getTableGrid());
|
||||
table.setRowHeight(24);
|
||||
|
||||
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
|
||||
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
|
||||
setBorder(new EmptyBorder(2, 6, 2, 6));
|
||||
if (isSel) {
|
||||
setBackground(getSelectionBg());
|
||||
setForeground(getSelectionFg());
|
||||
} else {
|
||||
setBackground(row % 2 == 0 ? getTableRowEven() : getTableRowOdd());
|
||||
setForeground(getFgMain());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
if (header != null) {
|
||||
header.setBackground(getTableHeaderBg());
|
||||
header.setForeground(getTableHeaderFg());
|
||||
header.setFont(header.getFont().deriveFont(Font.BOLD));
|
||||
header.setDefaultRenderer(new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
|
||||
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
|
||||
setBackground(getTableHeaderBg());
|
||||
setForeground(getTableHeaderFg());
|
||||
setFont(getFont().deriveFont(Font.BOLD));
|
||||
setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
|
||||
new EmptyBorder(4, 6, 4, 6)));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
|
||||
if (tp == null) return null;
|
||||
tp.setUI(new StyledTabbedPaneUI());
|
||||
tp.setBackground(getBgMain());
|
||||
tp.setForeground(getFgMain());
|
||||
return tp;
|
||||
}
|
||||
|
||||
public static JMenuBar styleMenuBar(JMenuBar bar) {
|
||||
if (bar == null) return null;
|
||||
bar.setUI(new StyledMenuBarUI());
|
||||
bar.setBackground(getMenuBarBg());
|
||||
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
|
||||
return bar;
|
||||
}
|
||||
|
||||
public static JMenu styleMenu(JMenu menu) {
|
||||
if (menu == null) return null;
|
||||
menu.setUI(new StyledMenuUI());
|
||||
menu.setForeground(getMenuBarFg());
|
||||
menu.setBackground(getMenuBarBg());
|
||||
menu.setOpaque(false);
|
||||
return menu;
|
||||
}
|
||||
|
||||
public static JMenuItem styleMenuItem(JMenuItem item) {
|
||||
if (item == null) return null;
|
||||
item.setUI(new StyledMenuItemUI());
|
||||
item.setBackground(getMenuPopupBg());
|
||||
item.setForeground(getMenuItemFg());
|
||||
return item;
|
||||
}
|
||||
|
||||
public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
|
||||
if (popup == null) return null;
|
||||
popup.setUI(new StyledPopupMenuUI());
|
||||
popup.setBackground(getMenuPopupBg());
|
||||
popup.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(getBorder(), 1),
|
||||
new EmptyBorder(4, 0, 4, 0)));
|
||||
return popup;
|
||||
}
|
||||
|
||||
public static JScrollPane styleScrollPane(JScrollPane sp) {
|
||||
if (sp == null) return null;
|
||||
sp.setBackground(getBgPanel());
|
||||
if (sp.getViewport() != null) {
|
||||
sp.getViewport().setBackground(getBgComponent());
|
||||
}
|
||||
sp.setBorder(new LineBorder(getBorder(), 1));
|
||||
return sp;
|
||||
}
|
||||
|
||||
public static JSpinner styleSpinner(JSpinner sp) {
|
||||
if (sp == null) return null;
|
||||
sp.setBackground(getBgComponent());
|
||||
sp.setForeground(getFgMain());
|
||||
sp.setBorder(new LineBorder(getBorder(), 1));
|
||||
JComponent editor = sp.getEditor();
|
||||
if (editor instanceof JSpinner.DefaultEditor) {
|
||||
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
|
||||
tf.setBackground(getBgComponent());
|
||||
tf.setForeground(getFgMain());
|
||||
tf.setCaretColor(getFgMain());
|
||||
tf.setBorder(new EmptyBorder(2, 4, 2, 4));
|
||||
}
|
||||
return sp;
|
||||
}
|
||||
|
||||
public static JCheckBox styleCheckBox(JCheckBox cb) {
|
||||
if (cb == null) return null;
|
||||
cb.setOpaque(false);
|
||||
cb.setForeground(getFgMain());
|
||||
cb.setFocusPainted(false);
|
||||
return cb;
|
||||
}
|
||||
|
||||
public static JRadioButton styleRadioButton(JRadioButton rb) {
|
||||
if (rb == null) return null;
|
||||
rb.setOpaque(false);
|
||||
rb.setForeground(getFgMain());
|
||||
rb.setFocusPainted(false);
|
||||
return rb;
|
||||
}
|
||||
|
||||
public static Border createTitledBorder(String title) {
|
||||
return BorderFactory.createTitledBorder(
|
||||
new LineBorder(getBorder(), 1),
|
||||
title,
|
||||
TitledBorder.DEFAULT_JUSTIFICATION,
|
||||
TitledBorder.DEFAULT_POSITION,
|
||||
new Font(Font.SANS_SERIF, Font.BOLD, 12),
|
||||
getFgMain());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Recursive Component Styling
|
||||
// =========================================================================
|
||||
|
||||
public static void applyTheme(Component comp) {
|
||||
applyTheme(comp, currentTheme);
|
||||
}
|
||||
|
||||
public static void applyTheme(Component comp, UITheme theme) {
|
||||
if (comp == null) return;
|
||||
|
||||
// Skip swatch preview panels whose background is the user-configured color
|
||||
if ("colorSwatch".equals(comp.getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JMenuBar) {
|
||||
styleMenuBar((JMenuBar) comp);
|
||||
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
|
||||
applyTheme(((JMenuBar) comp).getMenu(i), theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JMenu) {
|
||||
styleMenu((JMenu) comp);
|
||||
JMenu menu = (JMenu) comp;
|
||||
for (int i = 0; i < menu.getItemCount(); i++) {
|
||||
JMenuItem item = menu.getItem(i);
|
||||
if (item != null) applyTheme(item, theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JMenuItem) {
|
||||
styleMenuItem((JMenuItem) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JPopupMenu) {
|
||||
stylePopupMenu((JPopupMenu) comp);
|
||||
for (Component child : ((JPopupMenu) comp).getComponents()) {
|
||||
applyTheme(child, theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTabbedPane) {
|
||||
styleTabbedPane((JTabbedPane) comp);
|
||||
JTabbedPane tp = (JTabbedPane) comp;
|
||||
for (int i = 0; i < tp.getTabCount(); i++) {
|
||||
applyTheme(tp.getComponentAt(i), theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTable) {
|
||||
styleTable((JTable) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JScrollPane) {
|
||||
styleScrollPane((JScrollPane) comp);
|
||||
JScrollPane sp = (JScrollPane) comp;
|
||||
if (sp.getViewport() != null) {
|
||||
applyTheme(sp.getViewport().getView(), theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JButton) {
|
||||
JButton btn = (JButton) comp;
|
||||
if (!(btn.getUI() instanceof StyledButtonUI)) {
|
||||
styleButton(btn, ButtonVariant.DEFAULT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JComboBox) {
|
||||
styleComboBox((JComboBox<?>) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JSpinner) {
|
||||
styleSpinner((JSpinner) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTextArea) {
|
||||
styleTextArea((JTextArea) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTextField) {
|
||||
styleTextField((JTextField) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JCheckBox) {
|
||||
styleCheckBox((JCheckBox) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JRadioButton) {
|
||||
styleRadioButton((JRadioButton) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JLabel) {
|
||||
comp.setForeground(getFgMain(theme));
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JPanel) {
|
||||
JPanel panel = (JPanel) comp;
|
||||
if (panel.isOpaque()) {
|
||||
panel.setBackground(getBgPanel(theme));
|
||||
}
|
||||
panel.setForeground(getFgMain(theme));
|
||||
Border b = panel.getBorder();
|
||||
if (b instanceof TitledBorder) {
|
||||
TitledBorder tb = (TitledBorder) b;
|
||||
tb.setTitleColor(getFgMain(theme));
|
||||
tb.setBorder(new LineBorder(getBorder(theme), 1));
|
||||
}
|
||||
} else if (comp instanceof Container) {
|
||||
comp.setBackground(getBgMain(theme));
|
||||
comp.setForeground(getFgMain(theme));
|
||||
}
|
||||
|
||||
if (comp instanceof Container) {
|
||||
for (Component child : ((Container) comp).getComponents()) {
|
||||
applyTheme(child, theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyThemeToWindow(Window window) {
|
||||
if (window == null) return;
|
||||
window.setBackground(getBgMain());
|
||||
if (window instanceof RootPaneContainer) {
|
||||
RootPaneContainer rpc = (RootPaneContainer) window;
|
||||
if (rpc.getContentPane() != null) {
|
||||
rpc.getContentPane().setBackground(getBgMain());
|
||||
applyTheme(rpc.getContentPane());
|
||||
}
|
||||
if (rpc.getRootPane() != null && rpc.getRootPane().getJMenuBar() != null) {
|
||||
applyTheme(rpc.getRootPane().getJMenuBar());
|
||||
}
|
||||
}
|
||||
window.repaint();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Custom UI Implementations
|
||||
// =========================================================================
|
||||
|
||||
public static class StyledButtonUI extends BasicButtonUI {
|
||||
private final ButtonVariant variant;
|
||||
private boolean hover = false;
|
||||
|
||||
public StyledButtonUI(ButtonVariant variant) {
|
||||
this.variant = variant != null ? variant : ButtonVariant.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installUI(JComponent c) {
|
||||
super.installUI(c);
|
||||
AbstractButton b = (AbstractButton) c;
|
||||
b.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
hover = true;
|
||||
b.repaint();
|
||||
}
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
hover = false;
|
||||
b.repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics g, JComponent c) {
|
||||
AbstractButton b = (AbstractButton) c;
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
int w = c.getWidth();
|
||||
int h = c.getHeight();
|
||||
|
||||
ButtonModel model = b.getModel();
|
||||
boolean pressed = model.isArmed() && model.isPressed();
|
||||
boolean enabled = b.isEnabled();
|
||||
|
||||
UITheme t = ThemeManager.getTheme();
|
||||
Color bg = enabled ? ThemeManager.getButtonBg(variant, t, hover, pressed) : (t == UITheme.DARK ? new Color(40, 40, 40) : new Color(220, 220, 220));
|
||||
Color fg = enabled ? ThemeManager.getButtonFg(variant, t) : ThemeManager.getFgDisabled(t);
|
||||
|
||||
// Background
|
||||
g2.setColor(bg);
|
||||
g2.fillRoundRect(0, 0, w, h, 6, 6);
|
||||
|
||||
// Border
|
||||
if (variant == ButtonVariant.DEFAULT || variant == ButtonVariant.CANCEL) {
|
||||
g2.setColor(ThemeManager.getBorder(t));
|
||||
g2.drawRoundRect(0, 0, w - 1, h - 1, 6, 6);
|
||||
}
|
||||
|
||||
// Text / Icon
|
||||
FontMetrics fm = g2.getFontMetrics(b.getFont());
|
||||
String text = b.getText();
|
||||
if (text != null && !text.isEmpty()) {
|
||||
g2.setColor(fg);
|
||||
g2.setFont(b.getFont());
|
||||
int tx = (w - fm.stringWidth(text)) / 2;
|
||||
int ty = (h - fm.getHeight()) / 2 + fm.getAscent();
|
||||
g2.drawString(text, tx, ty);
|
||||
}
|
||||
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledMenuBarUI extends BasicMenuBarUI {
|
||||
@Override
|
||||
public void paint(Graphics g, JComponent c) {
|
||||
g.setColor(ThemeManager.getMenuBarBg());
|
||||
g.fillRect(0, 0, c.getWidth(), c.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledMenuUI extends BasicMenuUI {
|
||||
@Override
|
||||
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getMenuItemHoverBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuBarBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getMenuItemHoverFg());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuBarFg());
|
||||
}
|
||||
super.paintText(g, menuItem, textRect, text);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledMenuItemUI extends BasicMenuItemUI {
|
||||
@Override
|
||||
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getSelectionBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuPopupBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getSelectionFg());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuItemFg());
|
||||
}
|
||||
super.paintText(g, menuItem, textRect, text);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledPopupMenuUI extends BasicPopupMenuUI {
|
||||
@Override
|
||||
public void paint(Graphics g, JComponent c) {
|
||||
g.setColor(ThemeManager.getMenuPopupBg());
|
||||
g.fillRect(0, 0, c.getWidth(), c.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledTabbedPaneUI extends BasicTabbedPaneUI {
|
||||
@Override
|
||||
protected void installDefaults() {
|
||||
super.installDefaults();
|
||||
tabInsets = new Insets(6, 16, 6, 16);
|
||||
selectedTabPadInsets = new Insets(2, 2, 2, 2);
|
||||
tabAreaInsets = new Insets(4, 4, 0, 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) {
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setColor(isSelected ? ThemeManager.getTabSelectedBg() : ThemeManager.getTabBg());
|
||||
g2.fillRoundRect(x, y, w, h + 4, 6, 6);
|
||||
g2.setColor(ThemeManager.getBorder());
|
||||
g2.drawRoundRect(x, y, w - 1, h + 4, 6, 6);
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintText(Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected) {
|
||||
g.setFont(font);
|
||||
g.setColor(isSelected ? ThemeManager.getTabSelectedFg() : ThemeManager.getTabFg());
|
||||
g.drawString(title, textRect.x, textRect.y + metrics.getAscent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
|
||||
int width = tabPane.getWidth();
|
||||
int height = tabPane.getHeight();
|
||||
Insets insets = tabPane.getInsets();
|
||||
int x = insets.left;
|
||||
int y = insets.top + calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||
int w = width - insets.right - insets.left;
|
||||
int h = height - insets.top - insets.bottom - calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||
|
||||
g.setColor(ThemeManager.getTabSelectedBg());
|
||||
g.fillRect(x, y, w, h);
|
||||
g.setColor(ThemeManager.getBorder());
|
||||
g.drawRect(x, y, w - 1, h - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
/**
|
||||
* Supported UI themes for the j3270 Java desktop interface.
|
||||
*/
|
||||
public enum UITheme {
|
||||
DARK("Dark Mode"),
|
||||
LIGHT("Light Mode");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
UITheme(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static UITheme fromString(String name) {
|
||||
if (name == null) return DARK;
|
||||
String s = name.trim().toUpperCase();
|
||||
if ("LIGHT".equals(s) || "LIGHT MODE".equals(s) || "LIGHT_MODE".equals(s)) {
|
||||
return LIGHT;
|
||||
}
|
||||
return DARK;
|
||||
}
|
||||
}
|
||||
@@ -26,25 +26,24 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
mainPanel.setBackground(new Color(30, 30, 30));
|
||||
|
||||
// Header
|
||||
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
|
||||
headerPanel.setBackground(new Color(30, 30, 30));
|
||||
headerPanel.setOpaque(false);
|
||||
JLabel iconLabel = new JLabel("⚠️");
|
||||
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
|
||||
headerPanel.add(iconLabel);
|
||||
|
||||
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
|
||||
titleBox.setBackground(new Color(30, 30, 30));
|
||||
titleBox.setOpaque(false);
|
||||
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
|
||||
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
|
||||
titleLabel.setForeground(new Color(255, 180, 80));
|
||||
titleLabel.setForeground(ThemeManager.getOiaFgWarn());
|
||||
titleBox.add(titleLabel);
|
||||
|
||||
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
|
||||
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
subtitleLabel.setForeground(new Color(180, 180, 180));
|
||||
subtitleLabel.setForeground(ThemeManager.getFgMuted());
|
||||
titleBox.add(subtitleLabel);
|
||||
headerPanel.add(titleBox);
|
||||
mainPanel.add(headerPanel, BorderLayout.NORTH);
|
||||
@@ -71,30 +70,27 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
JTextArea detailsArea = new JTextArea(sb.toString());
|
||||
detailsArea.setEditable(false);
|
||||
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
detailsArea.setBackground(new Color(20, 20, 20));
|
||||
detailsArea.setForeground(new Color(210, 210, 210));
|
||||
ThemeManager.styleTextArea(detailsArea);
|
||||
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(detailsArea);
|
||||
scrollPane.setPreferredSize(new Dimension(520, 260));
|
||||
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel Connection");
|
||||
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||
cancelBtn.setForeground(new Color(220, 220, 220));
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
accepted = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
JButton trustBtn = new JButton("Connect Anyway");
|
||||
trustBtn.setBackground(new Color(180, 100, 40));
|
||||
trustBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
trustBtn.addActionListener(e -> {
|
||||
accepted = true;
|
||||
@@ -106,6 +102,7 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
getRootPane().setDefaultButton(trustBtn);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ThemeManagerTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUIThemeEnum() {
|
||||
assertEquals("Dark Mode", UITheme.DARK.getDisplayName());
|
||||
assertEquals("Light Mode", UITheme.LIGHT.getDisplayName());
|
||||
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("DARK"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("dark"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("Dark Mode"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("LIGHT"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("light"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("Light Mode"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("invalid_value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColorTokensContrast() {
|
||||
for (UITheme theme : UITheme.values()) {
|
||||
Color bg = ThemeManager.getBgMain(theme);
|
||||
Color fg = ThemeManager.getFgMain(theme);
|
||||
assertNotNull(bg);
|
||||
assertNotNull(fg);
|
||||
|
||||
// Compute luminance contrast
|
||||
double bgLum = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue());
|
||||
double fgLum = (0.299 * fg.getRed() + 0.587 * fg.getGreen() + 0.114 * fg.getBlue());
|
||||
double lumDiff = Math.abs(bgLum - fgLum);
|
||||
|
||||
// Minimum luminance difference to guarantee readability
|
||||
assertTrue(lumDiff > 120, "Theme " + theme + " text contrast luminance difference " + lumDiff + " must be > 120");
|
||||
|
||||
// Verify Menu Bar colors
|
||||
Color menuBg = ThemeManager.getMenuBarBg(theme);
|
||||
Color menuFg = ThemeManager.getMenuItemFg(theme);
|
||||
double menuBgLum = (0.299 * menuBg.getRed() + 0.587 * menuBg.getGreen() + 0.114 * menuBg.getBlue());
|
||||
double menuFgLum = (0.299 * menuFg.getRed() + 0.587 * menuFg.getGreen() + 0.114 * menuFg.getBlue());
|
||||
assertTrue(Math.abs(menuBgLum - menuFgLum) > 100, "Menu contrast in " + theme + " must be high");
|
||||
|
||||
// Verify Status Bar colors
|
||||
Color sbBg = ThemeManager.getStatusBarBg(theme);
|
||||
Color sbNormal = ThemeManager.getOiaFgNormal(theme);
|
||||
assertNotNull(sbBg);
|
||||
assertNotNull(sbNormal);
|
||||
|
||||
// Verify Button colors
|
||||
Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme);
|
||||
Color btnDefFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.DEFAULT, theme);
|
||||
assertNotNull(btnDefBg);
|
||||
assertNotNull(btnDefFg);
|
||||
|
||||
Color btnPriBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.PRIMARY, theme);
|
||||
Color btnPriFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.PRIMARY, theme);
|
||||
assertNotNull(btnPriBg);
|
||||
assertNotNull(btnPriFg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThemeChangeListeners() {
|
||||
AtomicReference<UITheme> notifiedTheme = new AtomicReference<>(null);
|
||||
ThemeManager.addThemeChangeListener(notifiedTheme::set);
|
||||
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
assertEquals(UITheme.LIGHT, ThemeManager.getTheme());
|
||||
assertEquals(UITheme.LIGHT, notifiedTheme.get());
|
||||
assertTrue(ThemeManager.isLight());
|
||||
assertFalse(ThemeManager.isDark());
|
||||
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
assertEquals(UITheme.DARK, ThemeManager.getTheme());
|
||||
assertEquals(UITheme.DARK, notifiedTheme.get());
|
||||
assertTrue(ThemeManager.isDark());
|
||||
assertFalse(ThemeManager.isLight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComponentStyling() {
|
||||
JButton btn = new JButton("Test");
|
||||
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
assertNotNull(btn.getUI());
|
||||
assertTrue(btn.getUI() instanceof ThemeManager.StyledButtonUI);
|
||||
|
||||
JTextField tf = new JTextField("Test");
|
||||
ThemeManager.styleTextField(tf);
|
||||
assertNotNull(tf.getCaretColor());
|
||||
|
||||
JTextArea ta = new JTextArea("Test area");
|
||||
ThemeManager.styleTextArea(ta);
|
||||
assertNotNull(ta.getCaretColor());
|
||||
|
||||
JComboBox<String> cb = new JComboBox<>(new String[]{"A", "B"});
|
||||
ThemeManager.styleComboBox(cb);
|
||||
assertNotNull(cb.getUI());
|
||||
|
||||
JTable table = new JTable(new DefaultTableModel(new Object[]{"Col1"}, 1));
|
||||
ThemeManager.styleTable(table);
|
||||
assertNotNull(table.getSelectionBackground());
|
||||
|
||||
JTabbedPane tp = new JTabbedPane();
|
||||
tp.addTab("Tab1", new JPanel());
|
||||
ThemeManager.styleTabbedPane(tp);
|
||||
assertNotNull(tp.getUI());
|
||||
|
||||
JMenuBar mb = new JMenuBar();
|
||||
ThemeManager.styleMenuBar(mb);
|
||||
|
||||
JMenu menu = new JMenu("File");
|
||||
ThemeManager.styleMenu(menu);
|
||||
|
||||
JMenuItem mi = new JMenuItem("Open");
|
||||
ThemeManager.styleMenuItem(mi);
|
||||
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
ThemeManager.stylePopupMenu(popup);
|
||||
|
||||
JCheckBox chk = new JCheckBox("Check");
|
||||
ThemeManager.styleCheckBox(chk);
|
||||
|
||||
JRadioButton rb = new JRadioButton("Radio");
|
||||
ThemeManager.styleRadioButton(rb);
|
||||
|
||||
Border tb = ThemeManager.createTitledBorder("Title");
|
||||
assertNotNull(tb);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveApplyTheme() {
|
||||
JPanel root = new JPanel(new BorderLayout());
|
||||
JButton b = new JButton("OK");
|
||||
JTextField f = new JTextField("Data");
|
||||
JTabbedPane tp = new JTabbedPane();
|
||||
JPanel tabContent = new JPanel();
|
||||
JLabel lbl = new JLabel("Label");
|
||||
tabContent.add(lbl);
|
||||
tp.addTab("T1", tabContent);
|
||||
|
||||
root.add(b, BorderLayout.NORTH);
|
||||
root.add(f, BorderLayout.CENTER);
|
||||
root.add(tp, BorderLayout.SOUTH);
|
||||
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
ThemeManager.applyTheme(root);
|
||||
|
||||
assertEquals(ThemeManager.getBgPanel(UITheme.LIGHT), root.getBackground());
|
||||
assertEquals(ThemeManager.getFgMain(UITheme.LIGHT), lbl.getForeground());
|
||||
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
ThemeManager.applyTheme(root);
|
||||
|
||||
assertEquals(ThemeManager.getBgPanel(UITheme.DARK), root.getBackground());
|
||||
assertEquals(ThemeManager.getFgMain(UITheme.DARK), lbl.getForeground());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsPersistence() throws Exception {
|
||||
Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
|
||||
|
||||
File tmpIni = File.createTempFile("j3270_test_config", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
// Write INI file with javaUiTheme = dark
|
||||
try (PrintWriter pw = new PrintWriter(tmpIni)) {
|
||||
pw.println("[j3270]");
|
||||
pw.println("javaUiTheme = dark");
|
||||
pw.println("fontFamily = Monospaced");
|
||||
pw.println("fontSize = 16");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
|
||||
assertEquals(UITheme.DARK, Settings.getJavaUiTheme());
|
||||
|
||||
// Switch to light and export
|
||||
Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
File exportFile = File.createTempFile("j3270_export_config", ".ini");
|
||||
exportFile.deleteOnExit();
|
||||
|
||||
Settings.exportToIniFile(exportFile.getAbsolutePath());
|
||||
String exportedContent = new String(Files.readAllBytes(exportFile.toPath()));
|
||||
assertTrue(exportedContent.toLowerCase().contains("javauitheme = light"));
|
||||
|
||||
// Reload exported file
|
||||
Settings.loadFromIniFile(exportFile.getAbsolutePath());
|
||||
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
|
||||
} finally {
|
||||
tmpIni.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ package haus.nightmare.lib3270j;
|
||||
*/
|
||||
public class ConnectionConfig {
|
||||
|
||||
public enum ProxyType {
|
||||
NONE, HTTP, SOCKS4, SOCKS5
|
||||
}
|
||||
|
||||
private String host;
|
||||
private int port = 23;
|
||||
private TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
@@ -28,6 +32,21 @@ public class ConnectionConfig {
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||
private String codePage = "037";
|
||||
private String associatedPrinterLu = null;
|
||||
private boolean nvtLocalEcho = false;
|
||||
|
||||
// Proxy configuration
|
||||
private ProxyType proxyType = ProxyType.NONE;
|
||||
private String proxyHost = null;
|
||||
private int proxyPort = 0;
|
||||
private String proxyUsername = null;
|
||||
private String proxyPassword = null;
|
||||
|
||||
// STARTTLS (Telnet Option 46) dynamic socket elevation
|
||||
private boolean startTlsEnabled = true;
|
||||
|
||||
// RFC 1572 / RFC 2877 Environment variables (Express Logon)
|
||||
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
|
||||
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
@@ -101,6 +120,9 @@ public class ConnectionConfig {
|
||||
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
|
||||
}
|
||||
|
||||
public boolean isNvtLocalEcho() { return nvtLocalEcho; }
|
||||
public void setNvtLocalEcho(boolean nvtLocalEcho) { this.nvtLocalEcho = nvtLocalEcho; }
|
||||
|
||||
public String getTerminalName() { return terminalName; }
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
@@ -134,9 +156,58 @@ public class ConnectionConfig {
|
||||
this.dynamicCols = cols;
|
||||
}
|
||||
|
||||
public ProxyType getProxyType() { return proxyType; }
|
||||
public void setProxyType(ProxyType proxyType) { this.proxyType = proxyType != null ? proxyType : ProxyType.NONE; }
|
||||
|
||||
public String getProxyHost() { return proxyHost; }
|
||||
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
|
||||
|
||||
public int getProxyPort() { return proxyPort; }
|
||||
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
|
||||
|
||||
public String getProxyUsername() { return proxyUsername; }
|
||||
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
|
||||
|
||||
public String getProxyPassword() { return proxyPassword; }
|
||||
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
|
||||
|
||||
public void setProxy(ProxyType type, String host, int port, String username, String password) {
|
||||
this.proxyType = type != null ? type : ProxyType.NONE;
|
||||
this.proxyHost = host;
|
||||
this.proxyPort = port;
|
||||
this.proxyUsername = username;
|
||||
this.proxyPassword = password;
|
||||
}
|
||||
|
||||
public boolean isStartTlsEnabled() { return startTlsEnabled; }
|
||||
public void setStartTlsEnabled(boolean enabled) { this.startTlsEnabled = enabled; }
|
||||
|
||||
public java.util.Map<String, String> getEnvironmentVariables() { return environmentVariables; }
|
||||
public void setEnvironmentVariables(java.util.Map<String, String> vars) {
|
||||
this.environmentVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
|
||||
}
|
||||
public void setEnvironmentVariable(String name, String value) {
|
||||
if (name != null) {
|
||||
if (value != null) this.environmentVariables.put(name, value);
|
||||
else this.environmentVariables.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Map<String, String> getUserVariables() { return userVariables; }
|
||||
public void setUserVariables(java.util.Map<String, String> vars) {
|
||||
this.userVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
|
||||
}
|
||||
public void setUserVariable(String name, String value) {
|
||||
if (name != null) {
|
||||
if (value != null) this.userVariables.put(name, value);
|
||||
else this.userVariables.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
* or standard "host:port" formats.
|
||||
*/
|
||||
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||
@@ -146,6 +217,50 @@ public class ConnectionConfig {
|
||||
boolean tls = false;
|
||||
boolean tn3270e = true;
|
||||
|
||||
// Parse --proxy=<url> or -proxy=<url> flags
|
||||
ProxyType pType = ProxyType.NONE;
|
||||
String pHost = null;
|
||||
int pPort = 0;
|
||||
String pUser = null;
|
||||
String pPass = null;
|
||||
|
||||
String[] tokens = s.split("\\s+");
|
||||
StringBuilder remaining = new StringBuilder();
|
||||
for (String tok : tokens) {
|
||||
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
|
||||
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(proxyUrl);
|
||||
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
|
||||
if (scheme.equals("http") || scheme.equals("https")) {
|
||||
pType = ProxyType.HTTP;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
|
||||
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
|
||||
pType = ProxyType.SOCKS4;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
|
||||
pType = ProxyType.SOCKS5;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
}
|
||||
pHost = uri.getHost();
|
||||
String userInfo = uri.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
int colon = userInfo.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
pUser = userInfo.substring(0, colon);
|
||||
pPass = userInfo.substring(colon + 1);
|
||||
} else {
|
||||
pUser = userInfo;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} else {
|
||||
if (remaining.length() > 0) remaining.append(" ");
|
||||
remaining.append(tok);
|
||||
}
|
||||
}
|
||||
s = remaining.toString().trim();
|
||||
|
||||
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
|
||||
boolean prefixFound = true;
|
||||
while (prefixFound) {
|
||||
@@ -200,6 +315,9 @@ public class ConnectionConfig {
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
if (pType != ProxyType.NONE && pHost != null) {
|
||||
config.setProxy(pType, pHost, pPort, pUser, pPass);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ public class Telnet3270Client {
|
||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
|
||||
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
|
||||
this.inputProcessor.setOIA(oia);
|
||||
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
|
||||
|
||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||
@@ -118,6 +119,14 @@ public class Telnet3270Client {
|
||||
dsProcessor.addScreenUpdateListener(l);
|
||||
}
|
||||
|
||||
public void addSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
|
||||
fsm.addSCSInboundListener(l);
|
||||
}
|
||||
|
||||
public void removeSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
|
||||
fsm.removeSCSInboundListener(l);
|
||||
}
|
||||
|
||||
// ========== Screen access ==========
|
||||
|
||||
/** Get the screen buffer for rendering. */
|
||||
@@ -189,11 +198,17 @@ public class Telnet3270Client {
|
||||
fsm.sendNVTString(s);
|
||||
}
|
||||
|
||||
// ========== Convenience input methods ==========
|
||||
// ========== Convenience input methods & HoD compatibility ==========
|
||||
|
||||
/** Type a character at the cursor position. */
|
||||
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
|
||||
|
||||
/** Process a character at the current cursor position. */
|
||||
public void processChar(char ch) { inputProcessor.processChar(ch); }
|
||||
|
||||
/** Process a character with explicit position and insert mode flag. */
|
||||
public void processChar(char ch, int pos, boolean insert) { inputProcessor.processChar(ch, pos, insert); }
|
||||
|
||||
/** Type a string at the cursor position. */
|
||||
public void typeString(String s) {
|
||||
for (char ch : s.toCharArray()) {
|
||||
@@ -210,6 +225,7 @@ public class Telnet3270Client {
|
||||
public void sendEnter() {
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
}
|
||||
public void processEnter() { inputProcessor.processEnter(); }
|
||||
|
||||
/** Send a PF key (1-24). */
|
||||
public void sendPF(int number) {
|
||||
@@ -231,6 +247,7 @@ public class Telnet3270Client {
|
||||
}
|
||||
inputProcessor.sendAid(aid);
|
||||
}
|
||||
public void processPF(int number) { inputProcessor.processPF(number); }
|
||||
|
||||
/** Send a PA key (1-3). */
|
||||
public void sendPA(int number) {
|
||||
@@ -243,44 +260,104 @@ public class Telnet3270Client {
|
||||
}
|
||||
inputProcessor.sendAid(aid);
|
||||
}
|
||||
public void processPA(int number) { inputProcessor.processPA(number); }
|
||||
|
||||
/** Send Clear key. */
|
||||
public void sendClear() {
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||
}
|
||||
public void processClear() { inputProcessor.processClear(); }
|
||||
|
||||
/** Move cursor up. */
|
||||
public void cursorUp() { inputProcessor.cursorUp(); }
|
||||
public void cursorUp() { inputProcessor.cursorUp(); }
|
||||
public void processCursorUp() { inputProcessor.processCursorUp(); }
|
||||
|
||||
/** Move cursor down. */
|
||||
public void cursorDown() { inputProcessor.cursorDown(); }
|
||||
public void cursorDown() { inputProcessor.cursorDown(); }
|
||||
public void processCursorDown() { inputProcessor.processCursorDown(); }
|
||||
|
||||
/** Move cursor left. */
|
||||
public void cursorLeft() { inputProcessor.cursorLeft(); }
|
||||
public void cursorLeft() { inputProcessor.cursorLeft(); }
|
||||
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
|
||||
|
||||
/** Move cursor right. */
|
||||
public void cursorRight() { inputProcessor.cursorRight(); }
|
||||
public void cursorRight() { inputProcessor.cursorRight(); }
|
||||
public void processCursorRight() { inputProcessor.processCursorRight(); }
|
||||
|
||||
/** Move cursor to home position. */
|
||||
public void cursorHome() { inputProcessor.cursorHome(); }
|
||||
public void cursorHome() { inputProcessor.cursorHome(); }
|
||||
public void processHome() { inputProcessor.processHome(); }
|
||||
|
||||
/** Tab to next unprotected field. */
|
||||
public void tab() { inputProcessor.tab(); }
|
||||
public void tab() { inputProcessor.tab(); }
|
||||
public void processTab() { inputProcessor.processTab(); }
|
||||
|
||||
/** Back-tab to previous unprotected field. */
|
||||
public void backTab() { inputProcessor.backTab(); }
|
||||
public void backTab() { inputProcessor.backTab(); }
|
||||
public void processBackTab() { inputProcessor.processBackTab(); }
|
||||
|
||||
/** Move cursor to next line. */
|
||||
public void newline() { inputProcessor.newline(); }
|
||||
public void newline() { inputProcessor.newline(); }
|
||||
public void processNewline() { inputProcessor.processNewline(); }
|
||||
|
||||
/** Delete character under cursor. */
|
||||
public void deleteChar() { inputProcessor.deleteChar(); }
|
||||
public void processDelete() { inputProcessor.processDelete(); }
|
||||
|
||||
/** Backspace character before cursor. */
|
||||
public void backspace() { inputProcessor.backspace(); }
|
||||
public void processBackspace() { inputProcessor.processBackspace(); }
|
||||
|
||||
/** Erase to end of field. */
|
||||
public void eraseEof() { inputProcessor.eraseEof(); }
|
||||
public void processEraseEOF() { inputProcessor.processEraseEOF(); }
|
||||
|
||||
/** Erase all unprotected fields. */
|
||||
public void eraseInput() { inputProcessor.eraseInput(); }
|
||||
public void eraseInput() { inputProcessor.eraseInput(); }
|
||||
public void processEraseInput() { inputProcessor.processEraseInput(); }
|
||||
|
||||
/** Insert Duplicate order. */
|
||||
public void dup() { inputProcessor.dup(); }
|
||||
public void processDup() { inputProcessor.processDup(); }
|
||||
|
||||
/** Insert Field Mark order. */
|
||||
public void fieldMark() { inputProcessor.fieldMark(); }
|
||||
public void fieldMark() { inputProcessor.fieldMark(); }
|
||||
public void processFieldMark() { inputProcessor.processFieldMark(); }
|
||||
|
||||
/** Toggle Insert Mode. */
|
||||
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
|
||||
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
|
||||
|
||||
/** Move word left. */
|
||||
public void processWordLeft() { inputProcessor.processWordLeft(); }
|
||||
|
||||
/** Move word right. */
|
||||
public void processWordRight() { inputProcessor.processWordRight(); }
|
||||
|
||||
/** Move to field end. */
|
||||
public void processFieldEnd() { inputProcessor.processFieldEnd(); }
|
||||
|
||||
/** Attention key. */
|
||||
public void attn() { inputProcessor.attn(); }
|
||||
public void processAttn() { inputProcessor.processAttn(); }
|
||||
|
||||
/** SysReq key. */
|
||||
public void sysReq() { inputProcessor.sysReq(); }
|
||||
/** Reset (unlock keyboard). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
public void sysReq() { inputProcessor.sysReq(); }
|
||||
public void processSysReq() { inputProcessor.processSysReq(); }
|
||||
|
||||
/** Reset (unlock keyboard, reset OIA). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
public void processReset() { inputProcessor.processReset(); }
|
||||
|
||||
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
|
||||
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
||||
/** Trigger Light Pen selection at the specified screen address. */
|
||||
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
||||
public boolean processCurSel() { return inputProcessor.processCurSel(); }
|
||||
public boolean processCursorSelect() { return inputProcessor.processCursorSelect(); }
|
||||
|
||||
/** Trigger Light Pen selection at the specified screen address or cursor position. */
|
||||
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
|
||||
public boolean processLightPen() { return inputProcessor.processLightPen(); }
|
||||
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return dsProcessor.getProgramSymbolManager();
|
||||
|
||||
@@ -164,10 +164,11 @@ public class EbcdicTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
|
||||
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification.
|
||||
*/
|
||||
public char getAplGraphic(int ec) {
|
||||
switch (ec & 0xFF) {
|
||||
public char mapAPL(int ebcdicCodePoint) {
|
||||
switch (ebcdicCodePoint & 0xFF) {
|
||||
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
|
||||
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
|
||||
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
|
||||
@@ -198,7 +199,14 @@ public class EbcdicTranslator {
|
||||
case 0xBF: return '\u00B5'; // Micro 'µ'
|
||||
case 0x5F: return '\u00AC'; // Not sign '¬'
|
||||
|
||||
default: return ebcdicToUnicode(ec & 0xFF);
|
||||
default: return ebcdicToUnicode(ebcdicCodePoint & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||
*/
|
||||
public char getAplGraphic(int ec) {
|
||||
return mapAPL(ec);
|
||||
}
|
||||
}
|
||||
|
||||
+305
-98
@@ -144,6 +144,9 @@ public class DataStreamProcessor {
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
graphicsPlane.clear();
|
||||
if (gocaDecoder != null) {
|
||||
gocaDecoder.setGraphicsCursorActive(false);
|
||||
}
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
@@ -161,6 +164,9 @@ public class DataStreamProcessor {
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
graphicsPlane.clear();
|
||||
if (gocaDecoder != null) {
|
||||
gocaDecoder.setGraphicsCursorActive(false);
|
||||
}
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
@@ -510,11 +516,14 @@ public class DataStreamProcessor {
|
||||
|
||||
// Handle GE (graphic escape) prefix
|
||||
byte fillCs = currentCs;
|
||||
char fillUcs4 = 0;
|
||||
if (pos + 4 < end && fillChar == ORDER_GE) {
|
||||
fillChar = data[pos + 4] & 0xFF;
|
||||
fillCs = CS_GE;
|
||||
fillUcs4 = (translator != null) ? translator.mapAPL(fillChar) : (char) fillChar;
|
||||
pos += 5;
|
||||
} else {
|
||||
fillUcs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar) : (char) fillChar;
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
@@ -523,6 +532,7 @@ public class DataStreamProcessor {
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0; // Destroy previous field attribute if any
|
||||
ea.ec = (byte) fillChar;
|
||||
ea.ucs4 = fillUcs4;
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
@@ -575,6 +585,7 @@ public class DataStreamProcessor {
|
||||
ea.fa = 0; // Destroy previous field attribute if any
|
||||
ea.ec = (byte) geChar;
|
||||
ea.cs = CS_GE;
|
||||
ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar;
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
@@ -680,7 +691,7 @@ public class DataStreamProcessor {
|
||||
|
||||
// ========== Read Buffer ==========
|
||||
|
||||
private void processReadBuffer() {
|
||||
public void processReadBuffer() {
|
||||
outputPos = 0;
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
@@ -780,11 +791,21 @@ public class DataStreamProcessor {
|
||||
|
||||
// ========== Read Modified ==========
|
||||
|
||||
private void processReadModified(boolean all) {
|
||||
public void processReadModified(boolean all) {
|
||||
if (ftDft != null && ftDft.readModified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputProcessor != null) {
|
||||
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
|
||||
if (outputSender != null) {
|
||||
outputSender.send3270Data(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
outputPos = 0;
|
||||
|
||||
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||
@@ -843,10 +864,11 @@ public class DataStreamProcessor {
|
||||
sendOutput();
|
||||
}
|
||||
|
||||
// ========== Write Structured Field ==========
|
||||
|
||||
private void processWriteStructuredField(byte[] data, int offset, int length) {
|
||||
int pos = offset + 1; // Skip WSF command byte
|
||||
public void processWriteStructuredField(byte[] data, int offset, int length) {
|
||||
if (data == null || length <= 0)
|
||||
return;
|
||||
int firstByte = data[offset] & 0xFF;
|
||||
int pos = (firstByte == CMD_WSF || firstByte == SNA_CMD_WSF) ? offset + 1 : offset;
|
||||
int end = offset + length;
|
||||
|
||||
while (pos < end) {
|
||||
@@ -881,10 +903,19 @@ public class DataStreamProcessor {
|
||||
case SF_ACTIVATE_PART:
|
||||
processActivatePartition(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_OUTBOUND_DS:
|
||||
case SF_SET_WINDOW: // 0x0F: Set Window / Modify Partition / 3270 Graphics Viewport
|
||||
processSFSetWindow(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_3270_GRAPHICS: // 0x20: 3270 Graphics / Object Control
|
||||
processSFObjectControl(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_DOCUMENT_DATA: // 0x24: Document Data / embedded SCS / Object Control
|
||||
processSFDocumentData(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_OUTBOUND_DS: // 0x40
|
||||
processOutbound3270DS(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_TRANSFER_DATA:
|
||||
case SF_TRANSFER_DATA: // 0xD0
|
||||
if (ftDft != null) {
|
||||
ftDft.processStructuredField(data, pos, fieldLen);
|
||||
} else {
|
||||
@@ -898,95 +929,7 @@ public class DataStreamProcessor {
|
||||
programSymbolManager.loadps(psData);
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
||||
if (fieldLen >= 4) {
|
||||
int sfSubId = data[pos + 3] & 0xFF;
|
||||
switch (sfSubId) {
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
||||
if (fieldLen > 4) {
|
||||
byte[] psData = new byte[fieldLen - 4];
|
||||
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
||||
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
|
||||
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
|
||||
gocaDecoder.setGraphicsCursorActive(true);
|
||||
notifyScreenUpdated();
|
||||
break;
|
||||
}
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
|
||||
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
||||
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
||||
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
||||
|
||||
StringBuilder hexDump = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(32, fieldLen); i++) {
|
||||
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
|
||||
}
|
||||
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
|
||||
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
|
||||
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
int targetW = screen.getCols() * 9;
|
||||
int targetH = screen.getRows() * 16;
|
||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||
graphicsPlane.resize(targetW, targetH);
|
||||
}
|
||||
|
||||
if (flags == 0x80) { // SPAN_FIRST
|
||||
gocaAccumulator.reset();
|
||||
currentGocaSubtype = sfSubId;
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x00) { // SPAN_MIDDLE
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x40) { // SPAN_LAST
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||
gocaAccumulator.reset();
|
||||
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
||||
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
} else { // SPAN_ONLY (0xC0)
|
||||
gocaAccumulator.reset();
|
||||
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
||||
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||
@@ -1274,11 +1217,275 @@ public class DataStreamProcessor {
|
||||
return pid >= 0 && pid <= 255;
|
||||
}
|
||||
|
||||
// ========== Missing Structured Field Handlers (Phase 2) ==========
|
||||
|
||||
public void processSFSetWindow(byte[] data, int offset, int fieldLen) {
|
||||
if (fieldLen >= 4) {
|
||||
int sfSubId = data[offset + 3] & 0xFF;
|
||||
switch (sfSubId) {
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
||||
if (fieldLen > 4) {
|
||||
byte[] psData = new byte[fieldLen - 4];
|
||||
System.arraycopy(data, offset + 4, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
||||
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
|
||||
gocaDecoder.setGraphicsCursorActive(true);
|
||||
notifyScreenUpdated();
|
||||
break;
|
||||
}
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
|
||||
int flags = (fieldLen >= 6) ? (data[offset + 5] & 0xC0) : 0xC0;
|
||||
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
|
||||
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
|
||||
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
int targetW = screen.getCols() * 9;
|
||||
int targetH = screen.getRows() * 16;
|
||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||
graphicsPlane.resize(targetW, targetH);
|
||||
}
|
||||
|
||||
if (flags == 0x80) { // SPAN_FIRST
|
||||
gocaAccumulator.reset();
|
||||
currentGocaSubtype = sfSubId;
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x00) { // SPAN_MIDDLE
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x40) { // SPAN_LAST
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||
gocaAccumulator.reset();
|
||||
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
||||
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
} else { // SPAN_ONLY (0xC0)
|
||||
gocaAccumulator.reset();
|
||||
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
||||
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (fieldLen >= 11) {
|
||||
int xMin = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
|
||||
int yMin = ((data[offset + 5] & 0xFF) << 8) | (data[offset + 6] & 0xFF);
|
||||
int xMax = ((data[offset + 7] & 0xFF) << 8) | (data[offset + 8] & 0xFF);
|
||||
int yMax = ((data[offset + 9] & 0xFF) << 8) | (data[offset + 10] & 0xFF);
|
||||
graphicsPlane.setViewingWindow(xMin, yMin, xMax, yMax);
|
||||
}
|
||||
log.fine("SF 0x0F Set Window processed (len=" + fieldLen + ")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void processSFSetWindow(byte[] sf) {
|
||||
if (sf != null && sf.length >= 3) {
|
||||
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processSFSetWindow(sf, off, sf.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processSFObjectControl(byte[] data, int offset, int fieldLen) {
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
gocaDecoder.decodeStream(data, offset + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
public void processSFObjectControl(byte[] sf) {
|
||||
if (sf != null && sf.length >= 3) {
|
||||
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processSFObjectControl(sf, off, sf.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processSFDocumentData(byte[] data, int offset, int fieldLen) {
|
||||
if (fieldLen > 3) {
|
||||
if (embeddedScsProcessor != null) {
|
||||
embeddedScsProcessor.processHostData(data, offset + 3, fieldLen - 3);
|
||||
}
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
gocaDecoder.processProcedureOrders(data, offset + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
public void processSFDocumentData(byte[] sf) {
|
||||
if (sf != null && sf.length >= 3) {
|
||||
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processSFDocumentData(sf, off, sf.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
|
||||
if (fieldLen > 5) {
|
||||
if (fieldLen >= 4) {
|
||||
int pid = data[offset + 3] & 0xFF;
|
||||
screen.setActivePartition(pid);
|
||||
processRecord(data, offset + 4, fieldLen - 4, false);
|
||||
if (fieldLen > 4) {
|
||||
processRecord(data, offset + 4, fieldLen - 4, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void processOutbound3270DS(byte[] sf) {
|
||||
if (sf != null && sf.length >= 4) {
|
||||
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processOutbound3270DS(sf, off, sf.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Convenience Overloads ==========
|
||||
|
||||
public void processRecord(byte[] data) {
|
||||
if (data != null && data.length > 0) {
|
||||
processRecord(data, 0, data.length, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void processWrite(byte[] data) {
|
||||
if (data != null && data.length > 0) {
|
||||
processRecord(data, 0, data.length, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void processEraseWrite(byte[] data) {
|
||||
if (data != null && data.length > 0) {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
graphicsPlane.clear();
|
||||
if (gocaDecoder != null) {
|
||||
gocaDecoder.setGraphicsCursorActive(false);
|
||||
}
|
||||
processWrite(data, 0, data.length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
public void processEraseWriteAlternate(byte[] data) {
|
||||
if (data != null && data.length > 0) {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
graphicsPlane.clear();
|
||||
if (gocaDecoder != null) {
|
||||
gocaDecoder.setGraphicsCursorActive(false);
|
||||
}
|
||||
processWrite(data, 0, data.length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
public void processEraseAllUnprotected() {
|
||||
synchronized (screen.getRenderLock()) {
|
||||
screen.eraseAllUnprotected();
|
||||
}
|
||||
}
|
||||
|
||||
public void processReadModified() {
|
||||
processReadModified(false);
|
||||
}
|
||||
|
||||
public void processReadModifiedAll() {
|
||||
processReadModified(true);
|
||||
}
|
||||
|
||||
public void processWriteStructuredField(byte[] data) {
|
||||
if (data != null && data.length > 0) {
|
||||
processWriteStructuredField(data, 0, data.length);
|
||||
}
|
||||
}
|
||||
|
||||
public void processSFReadPartition(byte[] data) {
|
||||
if (data != null && data.length >= 3) {
|
||||
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processSFReadPartition(data, off, data.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processSFReadPartitionQuery(byte[] data) {
|
||||
sendAllQueryReplies();
|
||||
}
|
||||
|
||||
public void processSFReadPartitionQueryList(byte[] data) {
|
||||
if (data != null && data.length >= 6) {
|
||||
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
int qlStart = off + 6;
|
||||
if (data.length > qlStart) {
|
||||
byte[] codes = new byte[data.length - qlStart];
|
||||
System.arraycopy(data, qlStart, codes, 0, codes.length);
|
||||
sendRequestedQueryReplies(codes);
|
||||
} else {
|
||||
sendAllQueryReplies();
|
||||
}
|
||||
} else {
|
||||
sendAllQueryReplies();
|
||||
}
|
||||
}
|
||||
|
||||
public void processSetReplyMode(byte[] data) {
|
||||
if (data != null && data.length >= 3) {
|
||||
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processSetReplyMode(data, off, data.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processCreatePartition(byte[] data) {
|
||||
if (data != null && data.length >= 3) {
|
||||
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processCreatePartition(data, off, data.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processDestroyPartition(byte[] data) {
|
||||
if (data != null && data.length >= 3) {
|
||||
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processDestroyPartition(data, off, data.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processActivatePartition(byte[] data) {
|
||||
if (data != null && data.length >= 3) {
|
||||
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processActivatePartition(data, off, data.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
public void processEraseReset(byte[] data) {
|
||||
if (data != null && data.length >= 3) {
|
||||
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||
processEraseReset(data, off, data.length - off);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ public class QueryReplyBuilder {
|
||||
out.write(data, 0, data.length);
|
||||
}
|
||||
|
||||
private byte[] buildSummary() {
|
||||
public byte[] buildSummary() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||
@@ -307,7 +307,13 @@ public class QueryReplyBuilder {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
||||
public byte[] buildUsableArea() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildUsableArea(maxCols, maxRows, maxCols * maxRows);
|
||||
}
|
||||
|
||||
public byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
||||
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
|
||||
out.write(0x00); // no special character features
|
||||
@@ -316,12 +322,12 @@ public class QueryReplyBuilder {
|
||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||
out.write(maxRows & 0xFF); // usable height low
|
||||
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
|
||||
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
|
||||
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||
out.write((Xr_HOD >> 24) & 0xFF);
|
||||
out.write((Xr_HOD >> 16) & 0xFF);
|
||||
out.write((Xr_HOD >> 8) & 0xFF);
|
||||
out.write(Xr_HOD & 0xFF);
|
||||
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
|
||||
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||
out.write((Yr_HOD >> 24) & 0xFF);
|
||||
out.write((Yr_HOD >> 16) & 0xFF);
|
||||
out.write((Yr_HOD >> 8) & 0xFF);
|
||||
@@ -359,8 +365,14 @@ public class QueryReplyBuilder {
|
||||
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||
}
|
||||
|
||||
private byte[] buildAlphaPartitions(int maxRows) {
|
||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||
public byte[] buildAlphaPartitions() {
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildAlphaPartitions(maxRows);
|
||||
}
|
||||
|
||||
public byte[] buildAlphaPartitions(int maxRows) {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int bufSize = maxCols * maxRows;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
|
||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||
@@ -369,7 +381,7 @@ public class QueryReplyBuilder {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
public byte[] buildCharsets() {
|
||||
int charW = getCharWidth();
|
||||
int charH = getCharHeight();
|
||||
|
||||
@@ -429,7 +441,7 @@ public class QueryReplyBuilder {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildColor() {
|
||||
public byte[] buildColor() {
|
||||
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
||||
return new byte[] {
|
||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||
@@ -443,7 +455,7 @@ public class QueryReplyBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildHighlighting() {
|
||||
public byte[] buildHighlighting() {
|
||||
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
||||
return new byte[] {
|
||||
0x04, 0x00, (byte) 0xF0,
|
||||
@@ -453,11 +465,15 @@ public class QueryReplyBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildReplyModes() {
|
||||
public byte[] buildReplyModes() {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
}
|
||||
|
||||
private byte[] buildDdm(int bufferSize) {
|
||||
public byte[] buildDdm() {
|
||||
return buildDdm(4096);
|
||||
}
|
||||
|
||||
public byte[] buildDdm(int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x00); // reserved
|
||||
@@ -470,7 +486,13 @@ public class QueryReplyBuilder {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
public byte[] buildImplicitPartition() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildImplicitPartition(maxCols, maxRows);
|
||||
}
|
||||
|
||||
public byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||
// Implicit partition sizes, 2 self-defining parameters
|
||||
|
||||
@@ -514,6 +536,12 @@ public class QueryReplyBuilder {
|
||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||
}
|
||||
|
||||
public byte[] buildSegment() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildSegment(maxCols, maxRows);
|
||||
}
|
||||
|
||||
public byte[] buildSegment(int maxCols, int maxRows) {
|
||||
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
|
||||
return new byte[]{
|
||||
@@ -524,10 +552,20 @@ public class QueryReplyBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
public byte[] buildGraphics() {
|
||||
return buildSegment();
|
||||
}
|
||||
|
||||
public byte[] buildGraphics(int maxCols, int maxRows) {
|
||||
return buildSegment(maxCols, maxRows);
|
||||
}
|
||||
|
||||
public byte[] buildProcedure() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildProcedure(maxCols, maxRows);
|
||||
}
|
||||
|
||||
public byte[] buildProcedure(int maxCols, int maxRows) {
|
||||
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
|
||||
return new byte[]{
|
||||
@@ -540,6 +578,10 @@ public class QueryReplyBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
public byte[] buildGImage() {
|
||||
return buildProcedure();
|
||||
}
|
||||
|
||||
public byte[] buildGImage(int maxCols, int maxRows) {
|
||||
return buildProcedure(maxCols, maxRows);
|
||||
}
|
||||
@@ -581,6 +623,16 @@ public class QueryReplyBuilder {
|
||||
appendPort(out);
|
||||
}
|
||||
|
||||
public byte[] buildPort() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
|
||||
appendPort(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public byte[] buildOemFormat() {
|
||||
return buildPort();
|
||||
}
|
||||
|
||||
public byte[] buildGrColor() {
|
||||
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||
@@ -602,6 +654,10 @@ public class QueryReplyBuilder {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public byte[] buildGraphicColor() {
|
||||
return buildGrColor();
|
||||
}
|
||||
|
||||
public byte[] buildGColor() {
|
||||
return buildGrColor();
|
||||
}
|
||||
|
||||
@@ -49,4 +49,23 @@ public interface ECLConstants {
|
||||
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
|
||||
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
|
||||
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
|
||||
|
||||
// Alphanumeric entry types (ECLOIA.getAlphanumericType())
|
||||
int TYPE_ALPHANUMERIC = 0;
|
||||
int TYPE_NUMERIC = 1;
|
||||
int TYPE_DBCS = 2;
|
||||
int ALPHANUMERIC_NORMAL = 0;
|
||||
int ALPHANUMERIC_NUMERIC = 1;
|
||||
int ALPHANUMERIC_DBCS = 2;
|
||||
|
||||
// Status condition flags
|
||||
int STATUS_READY = 0;
|
||||
int STATUS_X_SYSTEM = 1;
|
||||
int STATUS_X_NUM = 2;
|
||||
int STATUS_X_PROT = 3;
|
||||
int STATUS_X_WAIT = 4;
|
||||
int STATUS_X_INSERT = 5;
|
||||
int STATUS_X_COMM = 6;
|
||||
int STATUS_X_OVERFLOW = 7;
|
||||
int STATUS_X_OP = 8;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
@@ -56,24 +58,34 @@ public class ECLField {
|
||||
return cols > 0 ? endPos % cols : 0;
|
||||
}
|
||||
|
||||
private byte getLiveAttribute() {
|
||||
if (ps != null && ps.getScreenBuffer() != null) {
|
||||
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
|
||||
if (cell.isFieldAttribute()) {
|
||||
return cell.fa;
|
||||
}
|
||||
}
|
||||
return attribute;
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
return faIsModified(attribute & 0xFF);
|
||||
return faIsModified(getLiveAttribute() & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isProtected() {
|
||||
return faIsProtected(attribute & 0xFF);
|
||||
return faIsProtected(getLiveAttribute() & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isNumeric() {
|
||||
return faIsNumeric(attribute & 0xFF);
|
||||
return faIsNumeric(getLiveAttribute() & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isHighIntensity() {
|
||||
return faIsHigh(attribute & 0xFF);
|
||||
return faIsHigh(getLiveAttribute() & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return faIsZero(attribute & 0xFF);
|
||||
return faIsZero(getLiveAttribute() & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isDisplay() {
|
||||
@@ -81,11 +93,11 @@ public class ECLField {
|
||||
}
|
||||
|
||||
public boolean isPenSelectable() {
|
||||
return faIsSelectable(attribute & 0xFF);
|
||||
return faIsSelectable(getLiveAttribute() & 0xFF);
|
||||
}
|
||||
|
||||
public short getAttribute() {
|
||||
return (short) (attribute & 0xFF);
|
||||
return (short) (getLiveAttribute() & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,6 +152,80 @@ public class ECLField {
|
||||
}
|
||||
}
|
||||
|
||||
/** Return true if this field wraps from bottom of screen to top. */
|
||||
public boolean isWrapped() {
|
||||
return startPos > endPos;
|
||||
}
|
||||
|
||||
/** Last data buffer address (same as getEnd). */
|
||||
public int getDataEnd() {
|
||||
return endPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the specified buffer address is contained within this field (including its FA).
|
||||
*/
|
||||
public boolean contains(int pos) {
|
||||
if (ps == null) return false;
|
||||
int size = ps.getSize();
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
|
||||
if (startPos <= endPos) {
|
||||
return pos >= startPos && pos <= endPos;
|
||||
} else {
|
||||
// Wrapped field across screen boundary
|
||||
return pos >= startPos || pos <= endPos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the specified row and column is contained within this field.
|
||||
*/
|
||||
public boolean contains(int row, int col) {
|
||||
if (ps == null) return false;
|
||||
int cols = ps.getCols();
|
||||
return contains(row * cols + col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Modified Data Tag (MDT) for this field.
|
||||
*/
|
||||
public void setModified(boolean modified) {
|
||||
if (ps != null && ps.getScreenBuffer() != null) {
|
||||
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
|
||||
if (cell.isFieldAttribute()) {
|
||||
if (modified) {
|
||||
cell.fa = (byte) (cell.fa | FA_MODIFY);
|
||||
} else {
|
||||
cell.fa = (byte) (cell.fa & ~FA_MODIFY);
|
||||
}
|
||||
ps.getScreenBuffer().markAllChanged();
|
||||
ps.getScreenBuffer().updateDisplaySnapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all character data within this field to nulls.
|
||||
*/
|
||||
public void erase() {
|
||||
if (isProtected() || length <= 0 || ps == null || ps.getScreenBuffer() == null) return;
|
||||
ScreenBuffer sb = ps.getScreenBuffer();
|
||||
int size = sb.getRows() * sb.getCols();
|
||||
if (size <= 0) return;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int addr = (dataStart + i) % size;
|
||||
ExtendedAttribute ea = sb.getCell(addr);
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
}
|
||||
setModified(false);
|
||||
sb.markAllChanged();
|
||||
sb.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
|
||||
|
||||
@@ -123,6 +123,52 @@ public class ECLFieldList {
|
||||
return findField(row * cols + col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field preceding the given field in the field list.
|
||||
*/
|
||||
public synchronized ECLField getPreviousField(ECLField next) {
|
||||
if (next == null || fields.isEmpty()) return null;
|
||||
int idx = fields.indexOf(next);
|
||||
if (idx > 0) {
|
||||
return fields.get(idx - 1);
|
||||
} else if (idx == 0) {
|
||||
return fields.get(fields.size() - 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field at the given buffer position (alias for findField).
|
||||
*/
|
||||
public ECLField findFieldAt(int pos) {
|
||||
return findField(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field at the given row and column.
|
||||
*/
|
||||
public ECLField findFieldAt(int row, int col) {
|
||||
return findField(row, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field preceding the one at the given buffer position.
|
||||
*/
|
||||
public synchronized ECLField findPrevField(int pos) {
|
||||
ECLField curr = findField(pos);
|
||||
if (curr == null) return null;
|
||||
return getPreviousField(curr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field succeeding the one at the given buffer position.
|
||||
*/
|
||||
public synchronized ECLField findNextField(int pos) {
|
||||
ECLField curr = findField(pos);
|
||||
if (curr == null) return null;
|
||||
return getNextField(curr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find field containing the given text string.
|
||||
*/
|
||||
@@ -147,6 +193,13 @@ public class ECLFieldList {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
// Wrap around search to beginning of field list
|
||||
for (int i = 0; i < startIdx; i++) {
|
||||
ECLField f = fields.get(i);
|
||||
if (f.getText().contains(text)) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
@@ -73,6 +74,92 @@ public class ECLOIA implements ECLConstants {
|
||||
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
|
||||
}
|
||||
|
||||
private int inhibitOverride = INHIBIT_NOT_INHIBITED;
|
||||
|
||||
public void setInputInhibited(int reason) {
|
||||
if (this.inhibitOverride != reason) {
|
||||
this.inhibitOverride = reason;
|
||||
notifyOIAChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the alphanumeric character entry type allowed at current cursor position.
|
||||
* Returns TYPE_ALPHANUMERIC (0), TYPE_NUMERIC (1), or TYPE_DBCS (2).
|
||||
*/
|
||||
public int getAlphanumericType() {
|
||||
if (screen == null || !screen.isFormatted()) {
|
||||
return TYPE_ALPHANUMERIC;
|
||||
}
|
||||
int cur = screen.getCursorAddress();
|
||||
ExtendedAttribute ea = screen.getCell(cur);
|
||||
if (ea != null && (ea.cs == ExtendedAttribute.CS_DBCS || ea.db != 0)) {
|
||||
return TYPE_DBCS;
|
||||
}
|
||||
byte fa = screen.getFieldAttributeAt(cur);
|
||||
if (faIsNumeric(fa & 0xFF)) {
|
||||
return TYPE_NUMERIC;
|
||||
}
|
||||
return TYPE_ALPHANUMERIC;
|
||||
}
|
||||
|
||||
public String getAlphanumericTypeString() {
|
||||
switch (getAlphanumericType()) {
|
||||
case TYPE_NUMERIC: return "N";
|
||||
case TYPE_DBCS: return "D";
|
||||
case TYPE_ALPHANUMERIC:
|
||||
default: return "A";
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isXSystem() {
|
||||
return getInputInhibited() == INHIBIT_SYSTEM_LOCK;
|
||||
}
|
||||
|
||||
public boolean isXProt() {
|
||||
return getInputInhibited() == INHIBIT_PROTECTED_FIELD;
|
||||
}
|
||||
|
||||
public boolean isXNum() {
|
||||
return getInputInhibited() == INHIBIT_NUMERIC_ONLY;
|
||||
}
|
||||
|
||||
public boolean isXWait() {
|
||||
return isXSystem();
|
||||
}
|
||||
|
||||
public boolean isXInsert() {
|
||||
return isInsertMode();
|
||||
}
|
||||
|
||||
public boolean isXComm() {
|
||||
return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK;
|
||||
}
|
||||
|
||||
public boolean isXOverflow() {
|
||||
return getInputInhibited() == INHIBIT_OVERFLOW;
|
||||
}
|
||||
|
||||
public boolean isXOperatorDue() {
|
||||
return getInputInhibited() == INHIBIT_OPERATOR_DUE;
|
||||
}
|
||||
|
||||
public String getStatusString() {
|
||||
if (isCommError()) return "X-COMM";
|
||||
int inhibit = getInputInhibited();
|
||||
switch (inhibit) {
|
||||
case INHIBIT_SYSTEM_LOCK: return "X-SYSTEM";
|
||||
case INHIBIT_NUMERIC_ONLY: return "X-NUM";
|
||||
case INHIBIT_PROTECTED_FIELD: return "X-PROT";
|
||||
case INHIBIT_OVERFLOW: return "X-OVERFLOW";
|
||||
case INHIBIT_COMM_CHECK: return "X-COMM";
|
||||
case INHIBIT_OPERATOR_DUE: return "X-OP";
|
||||
default:
|
||||
if (isInsertMode()) return "X-INSERT";
|
||||
return "READY";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Input Inhibited code.
|
||||
* Returns one of INHIBIT_* constants from ECLConstants.
|
||||
@@ -81,6 +168,9 @@ public class ECLOIA implements ECLConstants {
|
||||
if (isCommError()) {
|
||||
return INHIBIT_COMM_CHECK;
|
||||
}
|
||||
if (inhibitOverride != INHIBIT_NOT_INHIBITED) {
|
||||
return inhibitOverride;
|
||||
}
|
||||
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
|
||||
return INHIBIT_SYSTEM_LOCK;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,19 @@ public class ECLPS implements ECLConstants {
|
||||
return fieldList;
|
||||
}
|
||||
|
||||
private boolean nvtMode = false;
|
||||
|
||||
public boolean isNVTmode() {
|
||||
if (inputProcessor != null && inputProcessor.isNvtMode()) {
|
||||
return true;
|
||||
}
|
||||
return nvtMode;
|
||||
}
|
||||
|
||||
public void setNVTmode(boolean nvt) {
|
||||
this.nvtMode = nvt;
|
||||
}
|
||||
|
||||
public int getSize() { return screen.getRows() * screen.getCols(); }
|
||||
public int getRows() { return screen.getRows(); }
|
||||
public int getCols() { return screen.getCols(); }
|
||||
@@ -139,8 +152,8 @@ public class ECLPS implements ECLConstants {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a string in the presentation space.
|
||||
* Returns 1-based or 0-based position, or -1 if not found.
|
||||
* Search for a string in the presentation space (0-based indexing).
|
||||
* Returns 0-based position, or -1 if not found.
|
||||
*/
|
||||
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
|
||||
if (target == null || target.isEmpty() || screen == null) return -1;
|
||||
@@ -161,7 +174,6 @@ public class ECLPS implements ECLConstants {
|
||||
|
||||
int targetLen = target.length();
|
||||
if (dir == SEARCH_FORWARD) {
|
||||
// Forward search with wrapping
|
||||
for (int i = 0; i < size; i++) {
|
||||
int pos = (startPos + i) % size;
|
||||
if (matchesAt(screenText, target, pos, size)) {
|
||||
@@ -169,7 +181,6 @@ public class ECLPS implements ECLConstants {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Backward search with wrapping
|
||||
for (int i = 0; i < size; i++) {
|
||||
int pos = (startPos - i + size) % size;
|
||||
if (matchesAt(screenText, target, pos, size)) {
|
||||
@@ -180,6 +191,78 @@ public class ECLPS implements ECLConstants {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int searchString(String target) {
|
||||
return searchString(target, 0, 0, SEARCH_FORWARD, false);
|
||||
}
|
||||
|
||||
public int searchString(String target, int startRow, int startCol) {
|
||||
return searchString(target, startRow, startCol, SEARCH_FORWARD, false);
|
||||
}
|
||||
|
||||
// ========== IBM HoD SearchPS / SearchPSExt (1-based API) ==========
|
||||
|
||||
public int SearchPS(String text) {
|
||||
return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true);
|
||||
}
|
||||
|
||||
public int SearchPS(String text, int startRow, int startCol) {
|
||||
int pos = (startRow - 1) * getCols() + startCol;
|
||||
return SearchPSExt(text, pos, getSize(), SEARCH_FORWARD, false, true);
|
||||
}
|
||||
|
||||
public int SearchPS(String text, int startRow, int startCol, int dir) {
|
||||
int pos = (startRow - 1) * getCols() + startCol;
|
||||
return SearchPSExt(text, pos, getSize(), dir, false, true);
|
||||
}
|
||||
|
||||
public int SearchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) {
|
||||
int pos = (startRow - 1) * getCols() + startCol;
|
||||
return SearchPSExt(text, pos, getSize(), dir, ignoreCase, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchPS extended method conforming to IBM ECL specification.
|
||||
* Uses 1-based positions and returns 1-based index (or 0 if not found).
|
||||
*/
|
||||
public synchronized int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return 0;
|
||||
|
||||
int s0 = Math.max(0, Math.min(startPos - 1, size - 1));
|
||||
int e0 = Math.max(0, Math.min(endPos - 1, size - 1));
|
||||
|
||||
char[] fullScreen = new char[size];
|
||||
getPlane(PLANE_TEXT, fullScreen, 0, size);
|
||||
String screenText = new String(fullScreen);
|
||||
|
||||
if (ignoreCase) {
|
||||
screenText = screenText.toLowerCase();
|
||||
text = text.toLowerCase();
|
||||
}
|
||||
|
||||
int count = wrap ? size : (dir == SEARCH_FORWARD ? (e0 >= s0 ? e0 - s0 + 1 : size - s0 + e0 + 1)
|
||||
: (s0 >= e0 ? s0 - e0 + 1 : s0 + size - e0 + 1));
|
||||
if (dir == SEARCH_FORWARD) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int pos = (s0 + i) % size;
|
||||
if (!wrap && e0 >= s0 && pos > e0) break;
|
||||
if (matchesAt(screenText, text, pos, size)) {
|
||||
return pos + 1; // 1-based
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int pos = (s0 - i + size) % size;
|
||||
if (!wrap && s0 >= e0 && pos < e0) break;
|
||||
if (matchesAt(screenText, text, pos, size)) {
|
||||
return pos + 1; // 1-based
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean matchesAt(String screenText, String target, int pos, int size) {
|
||||
int len = target.length();
|
||||
for (int j = 0; j < len; j++) {
|
||||
@@ -191,6 +274,62 @@ public class ECLPS implements ECLConstants {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== Rectangular Block Copy & Paste ==========
|
||||
|
||||
/**
|
||||
* Copy a rectangular text region from (sRow, sCol) to (eRow, eCol) inclusive.
|
||||
*/
|
||||
public synchronized String copyString(int sRow, int sCol, int eRow, int eCol) {
|
||||
if (screen == null) return "";
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
if (rows <= 0 || cols <= 0) return "";
|
||||
|
||||
int minR = Math.max(0, Math.min(sRow, eRow));
|
||||
int maxR = Math.min(rows - 1, Math.max(sRow, eRow));
|
||||
int minC = Math.max(0, Math.min(sCol, eCol));
|
||||
int maxC = Math.min(cols - 1, Math.max(sCol, eCol));
|
||||
int sliceWidth = maxC - minC + 1;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int r = minR; r <= maxR; r++) {
|
||||
char[] rowBuf = new char[sliceWidth];
|
||||
getPlane(PLANE_TEXT, rowBuf, r * cols + minC, sliceWidth);
|
||||
sb.append(rowBuf);
|
||||
if (r < maxR) {
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a multi-line rectangular block of text starting at (row, col).
|
||||
*/
|
||||
public synchronized int pasteString(String text, int row, int col) {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
if (rows <= 0 || cols <= 0) return 0;
|
||||
|
||||
String[] lines = text.split("\r?\n");
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
int targetRow = (row + i) % rows;
|
||||
String line = lines[i];
|
||||
for (int c = 0; c < line.length() && (col + c) < cols; c++) {
|
||||
int pos = targetRow * cols + (col + c);
|
||||
setCursorPos(pos);
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.typeCharacter(line.charAt(c));
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste text with line wrapping across unprotected fields.
|
||||
*/
|
||||
@@ -211,15 +350,15 @@ public class ECLPS implements ECLConstants {
|
||||
int curPos = screen.getCursorAddress();
|
||||
int curCol = curPos % cols;
|
||||
if (endCol > 0 && curCol >= endCol) {
|
||||
// Advance to next row
|
||||
int nextRow = (curPos / cols + 1) % rows;
|
||||
setCursorPos(nextRow * cols);
|
||||
}
|
||||
inputProcessor.typeCharacter(line.charAt(i));
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.typeCharacter(line.charAt(i));
|
||||
}
|
||||
charsPasted++;
|
||||
}
|
||||
if (l < lines.length - 1) {
|
||||
// Newline key between lines
|
||||
if (l < lines.length - 1 && inputProcessor != null) {
|
||||
inputProcessor.newline();
|
||||
}
|
||||
}
|
||||
@@ -234,4 +373,103 @@ public class ECLPS implements ECLConstants {
|
||||
inputProcessor.sendKeys(keys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send keystrokes with configurable inter-character or inter-mnemonic delay.
|
||||
*/
|
||||
public void sendCharacters(String keys, int delayMs) {
|
||||
if (keys == null || keys.isEmpty()) return;
|
||||
if (delayMs <= 0) {
|
||||
sendKeys(keys);
|
||||
return;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
int len = keys.length();
|
||||
while (i < len) {
|
||||
if (keys.charAt(i) == '[') {
|
||||
int close = keys.indexOf(']', i);
|
||||
if (close > i) {
|
||||
String mnemonic = keys.substring(i, close + 1);
|
||||
sendKeys(mnemonic);
|
||||
i = close + 1;
|
||||
} else {
|
||||
sendKeys(keys.substring(i, i + 1));
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
sendKeys(keys.substring(i, i + 1));
|
||||
i++;
|
||||
}
|
||||
|
||||
if (delayMs > 0 && i < len) {
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Synchronization & ECL Automation Waits ==========
|
||||
|
||||
/**
|
||||
* Block until the specified text appears anywhere on the presentation space.
|
||||
*/
|
||||
public boolean waitForScreen(String text, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (searchString(text) >= 0) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return searchString(text) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the specified text appears at the given (row, col) coordinate.
|
||||
*/
|
||||
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
String onScreen = getString(row, col, text.length());
|
||||
if (text.equals(onScreen)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return text.equals(getString(row, col, text.length()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the cursor moves to (row, col).
|
||||
*/
|
||||
public boolean waitForCursor(int row, int col, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getCursorRow() == row && getCursorCol() == col) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return getCursorRow() == row && getCursorCol() == col;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,21 +19,36 @@ public class FillArea {
|
||||
public static class Edge {
|
||||
public final double x1, y1;
|
||||
public final double x2, y2;
|
||||
public final int direction; // +1 if y1 < y2 (upward), -1 if y1 > y2 (downward)
|
||||
|
||||
public Edge(double x1, double y1, double x2, double y2) {
|
||||
this.x1 = x1;
|
||||
this.y1 = y1;
|
||||
this.x2 = x2;
|
||||
this.y2 = y2;
|
||||
this.direction = (y2 > y1) ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
private int fillRule = GocaConstants.FILL_RULE_EVEN_ODD;
|
||||
private final List<Edge> edges = new ArrayList<>();
|
||||
private final List<double[]> subpathsX = new ArrayList<>();
|
||||
private final List<double[]> subpathsY = new ArrayList<>();
|
||||
|
||||
public FillArea() {}
|
||||
|
||||
public FillArea(int fillRule) {
|
||||
this.fillRule = fillRule;
|
||||
}
|
||||
|
||||
public synchronized void setFillRule(int fillRule) {
|
||||
this.fillRule = fillRule;
|
||||
}
|
||||
|
||||
public synchronized int getFillRule() {
|
||||
return fillRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single directed edge to the edge table.
|
||||
*/
|
||||
@@ -104,12 +119,47 @@ public class FillArea {
|
||||
subpathsY.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rasterizes and fills a direct polygon on the target GraphicsPlane.
|
||||
*/
|
||||
public synchronized void fill(GraphicsPlane plane, int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb) {
|
||||
clear();
|
||||
addPolygon(px, py, numPoints);
|
||||
fill(plane, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
|
||||
*/
|
||||
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
|
||||
fill(plane, fillColorArgb, patternSet, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, this.fillRule, psm);
|
||||
}
|
||||
|
||||
private static class NodeIntersection implements Comparable<NodeIntersection> {
|
||||
final double x;
|
||||
final int dir;
|
||||
|
||||
NodeIntersection(double x, int dir) {
|
||||
this.x = x;
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(NodeIntersection other) {
|
||||
return Double.compare(this.x, other.x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
|
||||
*/
|
||||
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb, int fillRule, ProgramSymbolManager psm) {
|
||||
if (plane == null) return;
|
||||
if (edges.isEmpty() && subpathsX.isEmpty()) return;
|
||||
|
||||
@@ -140,7 +190,7 @@ public class FillArea {
|
||||
int iMinY = Math.max(0, (int) Math.floor(minY));
|
||||
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
|
||||
|
||||
List<Double> nodeX = new ArrayList<>();
|
||||
List<NodeIntersection> intersections = new ArrayList<>();
|
||||
byte[] patRows = null;
|
||||
ProgramSymbolSet.SymbolSlot psSlot = null;
|
||||
|
||||
@@ -156,45 +206,84 @@ public class FillArea {
|
||||
}
|
||||
|
||||
for (int y = iMinY; y <= iMaxY; y++) {
|
||||
nodeX.clear();
|
||||
intersections.clear();
|
||||
double scanY = y + 0.5;
|
||||
|
||||
for (Edge e : edges) {
|
||||
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
|
||||
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
|
||||
nodeX.add(x);
|
||||
intersections.add(new NodeIntersection(x, e.direction));
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(nodeX);
|
||||
Collections.sort(intersections);
|
||||
|
||||
for (int i = 0; i < nodeX.size(); i += 2) {
|
||||
if (i + 1 >= nodeX.size()) break;
|
||||
int leftX = Math.max(0, (int) Math.round(nodeX.get(i)));
|
||||
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1)));
|
||||
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
|
||||
// Non-Zero Winding Rule: evaluate winding count
|
||||
int winding = 0;
|
||||
for (int i = 0; i < intersections.size() - 1; i++) {
|
||||
winding += intersections.get(i).dir;
|
||||
if (winding != 0) {
|
||||
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
|
||||
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
|
||||
|
||||
for (int x = leftX; x <= rightX; x++) {
|
||||
if (psSlot != null) {
|
||||
int psW = psSlot.getWidth();
|
||||
int psH = psSlot.getHeight();
|
||||
int psX = (psW > 0) ? (x % psW) : 0;
|
||||
int psY = (psH > 0) ? (y % psH) : 0;
|
||||
byte[] psPix = psSlot.getPixelData();
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
plane.setPixel(x, y, bg);
|
||||
for (int x = leftX; x <= rightX; x++) {
|
||||
if (psSlot != null) {
|
||||
int psW = psSlot.getWidth();
|
||||
int psH = psSlot.getHeight();
|
||||
int psX = (psW > 0) ? (x % psW) : 0;
|
||||
int psY = (psH > 0) ? (y % psH) : 0;
|
||||
byte[] psPix = psSlot.getPixelData();
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
plane.setPixel(x, y, bg);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (patRows != null) {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
plane.setPixel(x, y, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (patRows != null) {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Even-Odd / Alternate Rule
|
||||
for (int i = 0; i < intersections.size(); i += 2) {
|
||||
if (i + 1 >= intersections.size()) break;
|
||||
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
|
||||
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
|
||||
|
||||
for (int x = leftX; x <= rightX; x++) {
|
||||
if (psSlot != null) {
|
||||
int psW = psSlot.getWidth();
|
||||
int psH = psSlot.getHeight();
|
||||
int psX = (psW > 0) ? (x % psW) : 0;
|
||||
int psY = (psH > 0) ? (y % psH) : 0;
|
||||
byte[] psPix = psSlot.getPixelData();
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
plane.setPixel(x, y, bg);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
plane.setPixel(x, y, bg);
|
||||
} else if (patRows != null) {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
plane.setPixel(x, y, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ public final class GocaConstants {
|
||||
public static final int G_GSMS = 0x1B; // Set Marker Size
|
||||
public static final int G_GSCP = 0x21; // Set Current Position
|
||||
public static final int G_GSAP = 0x22; // Arc Parameters
|
||||
public static final int G_GSC = 0x22; // Segment Characteristics
|
||||
public static final int G_GSVW_DEF = 0x23; // Set Viewing Window Definition
|
||||
public static final int G_GSECOL = 0x26; // Set Extended Color
|
||||
public static final int G_GSVW = 0x27; // Set Viewing Window
|
||||
public static final int G_GSPT = 0x28; // Set Pattern Symbol
|
||||
@@ -157,6 +159,19 @@ public final class GocaConstants {
|
||||
public static final int MIX_XOR = 4;
|
||||
public static final int MIX_UNDER = 5;
|
||||
|
||||
// Fill Rules (GBAR 0x68 flags)
|
||||
public static final int FILL_RULE_EVEN_ODD = 0;
|
||||
public static final int FILL_RULE_WINDING = 1;
|
||||
|
||||
// Image Formats & Compression (GBIMG 0xD1)
|
||||
public static final int IMG_UNCOMPRESSED = 0;
|
||||
public static final int IMG_RLE = 1;
|
||||
public static final int IMG_MMR = 2;
|
||||
public static final int BPP_1 = 1;
|
||||
public static final int BPP_2 = 2;
|
||||
public static final int BPP_4 = 4;
|
||||
public static final int BPP_8 = 8;
|
||||
|
||||
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
|
||||
public static final int[] GOCA_COLORS = new int[] {
|
||||
0xFF00FF00, // 0: Default (Green)
|
||||
|
||||
@@ -33,6 +33,8 @@ public class GocaDecoder {
|
||||
private int fillColor = GocaConstants.GOCA_COLORS[0];
|
||||
private int charDir = GocaConstants.CD_LR;
|
||||
private double charAngle = 0.0;
|
||||
private double charShear = 0.0;
|
||||
private double fractionalLineWidth = 1.0;
|
||||
private int charWidth = 9;
|
||||
private int charHeight = 16;
|
||||
private int charSet = 0;
|
||||
@@ -42,11 +44,16 @@ public class GocaDecoder {
|
||||
private int arcParamR = 0;
|
||||
private int arcParamS = 1;
|
||||
|
||||
private boolean segChained = false;
|
||||
private boolean segDynamic = false;
|
||||
private boolean segVisible = true;
|
||||
|
||||
private ProgramSymbolManager programSymbolManager;
|
||||
|
||||
// Area accumulation
|
||||
private boolean inArea = false;
|
||||
private boolean areaDrawBoundary = true;
|
||||
private int areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
|
||||
private boolean areaFill = true;
|
||||
private final List<Integer> areaPointsX = new ArrayList<>();
|
||||
private final List<Integer> areaPointsY = new ArrayList<>();
|
||||
@@ -59,6 +66,8 @@ public class GocaDecoder {
|
||||
private int imgY = 0;
|
||||
private int imgWidth = 0;
|
||||
private int imgHeight = 0;
|
||||
private int imgBitDepth = GocaConstants.BPP_1;
|
||||
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
private final List<Byte> imgBuffer = new ArrayList<>();
|
||||
|
||||
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
||||
@@ -198,6 +207,89 @@ public class GocaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public double getCharAngle() {
|
||||
return charAngle;
|
||||
}
|
||||
|
||||
public synchronized void setCharAngle(double angle) {
|
||||
this.charAngle = angle;
|
||||
}
|
||||
|
||||
public double getCharShear() {
|
||||
return charShear;
|
||||
}
|
||||
|
||||
public synchronized void setCharShear(double shear) {
|
||||
this.charShear = shear;
|
||||
}
|
||||
|
||||
public double getFractionalLineWidth() {
|
||||
return fractionalLineWidth;
|
||||
}
|
||||
|
||||
public synchronized void setFractionalLineWidth(double flw) {
|
||||
this.fractionalLineWidth = Math.max(0.1, flw);
|
||||
if (plane != null) {
|
||||
plane.setFractionalLineWidth(this.fractionalLineWidth);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSegChained() {
|
||||
return segChained;
|
||||
}
|
||||
|
||||
public boolean isSegDynamic() {
|
||||
return segDynamic;
|
||||
}
|
||||
|
||||
public boolean isSegVisible() {
|
||||
return segVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes GOCA order 0x23 / 0x27 Viewing Window clipping viewport.
|
||||
*/
|
||||
public synchronized void processViewingWindow(byte[] data) {
|
||||
if (data == null || data.length < 8) return;
|
||||
int xMin = readCoord(data, 0);
|
||||
int yMin = readCoord(data, 2);
|
||||
int xMax = readCoord(data, 4);
|
||||
int yMax = readCoord(data, 6);
|
||||
logger.info(String.format("GOCA processViewingWindow: [%d..%d, %d..%d]", xMin, xMax, yMin, yMax));
|
||||
if (plane != null) {
|
||||
plane.setViewingWindow(xMin, yMin, xMax, yMax);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes GOCA order 0x22 Segment Characteristics (chained/non-chained, dynamic, visible).
|
||||
*/
|
||||
public synchronized void processSegmentCharacteristics(byte[] data) {
|
||||
if (data == null || data.length < 1) return;
|
||||
int flags = data[0] & 0xFF;
|
||||
this.segChained = (flags & 0x80) != 0;
|
||||
this.segDynamic = (flags & 0x40) != 0;
|
||||
this.segVisible = (flags & 0x20) == 0;
|
||||
logger.info(String.format("GOCA processSegmentCharacteristics: flags=0x%02x (chained=%b, dynamic=%b, visible=%b)",
|
||||
flags, segChained, segDynamic, segVisible));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes GOCA order 0x11 Fractional Line Width calculation.
|
||||
*/
|
||||
public synchronized void processFractionalLineWidth(byte[] data) {
|
||||
if (data == null || data.length < 1) return;
|
||||
int intPart = data[0] & 0xFF;
|
||||
int fracPart = (data.length > 1) ? (data[1] & 0xFF) : 0;
|
||||
double flw = intPart + (fracPart / 256.0);
|
||||
if (flw <= 0.0) flw = 1.0;
|
||||
this.fractionalLineWidth = flw;
|
||||
if (plane != null) {
|
||||
plane.setFractionalLineWidth(flw);
|
||||
}
|
||||
logger.info("GOCA processFractionalLineWidth: flw=" + flw);
|
||||
}
|
||||
|
||||
public synchronized void resetDefaults() {
|
||||
curX = 0;
|
||||
curY = 0;
|
||||
@@ -217,6 +309,10 @@ public class GocaDecoder {
|
||||
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
|
||||
lineType = GocaConstants.LT_SOLID;
|
||||
lineWidth = GocaConstants.LW_NORMAL;
|
||||
fractionalLineWidth = 1.0;
|
||||
if (plane != null) {
|
||||
plane.setFractionalLineWidth(1.0);
|
||||
}
|
||||
markerType = GocaConstants.MK_PLUS;
|
||||
markerSize = 5;
|
||||
markerColor = curColor;
|
||||
@@ -226,14 +322,18 @@ public class GocaDecoder {
|
||||
fillColor = curColor;
|
||||
charDir = GocaConstants.CD_LR;
|
||||
charAngle = 0.0;
|
||||
charShear = 0.0;
|
||||
charSet = 0;
|
||||
charPrecision = GocaConstants.CP_STRING;
|
||||
inArea = false;
|
||||
areaDrawBoundary = true;
|
||||
areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
|
||||
areaFill = true;
|
||||
areaPointsX.clear();
|
||||
areaPointsY.clear();
|
||||
inImage = false;
|
||||
imgBitDepth = GocaConstants.BPP_1;
|
||||
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
imgBuffer.clear();
|
||||
}
|
||||
|
||||
@@ -270,6 +370,10 @@ public class GocaDecoder {
|
||||
if (idx + 1 >= end) {
|
||||
return -1;
|
||||
}
|
||||
// Fractional Line Width (0x11): 2-byte operand [int][frac] or 1-byte operand [int]
|
||||
if (order == GocaConstants.G_GSFLW) {
|
||||
return (idx + 2 < end) ? 3 : 2;
|
||||
}
|
||||
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
|
||||
if (order < 0x20) {
|
||||
return 2;
|
||||
@@ -497,7 +601,15 @@ public class GocaDecoder {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27)
|
||||
case GocaConstants.G_GSVW_DEF:
|
||||
case GocaConstants.G_GSVW: { // Set Viewing Window (0x23 / 0x27)
|
||||
if (payloadLen >= 8 && idx + 9 < end) {
|
||||
byte[] vwData = new byte[8];
|
||||
System.arraycopy(inputData, idx + 2, vwData, 0, 8);
|
||||
processViewingWindow(vwData);
|
||||
} else if (payloadLen == 0 && plane != null) {
|
||||
plane.clearViewingWindow();
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -558,6 +670,17 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
|
||||
if (payloadLen >= 4 && idx + 5 < end) {
|
||||
int sx = readCoord(inputData, idx + 2);
|
||||
int sy = readCoord(inputData, idx + 4);
|
||||
if (sx != 0 || sy != 0) {
|
||||
charShear = Math.toDegrees(Math.atan2(sx, sy));
|
||||
}
|
||||
} else if (payloadLen >= 2 && idx + 3 < end) {
|
||||
int intPart = inputData[idx + 2];
|
||||
int fracPart = (payloadLen >= 2) ? (inputData[idx + 3] & 0xFF) : 0;
|
||||
charShear = (intPart + fracPart / 256.0) * 45.0;
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -581,8 +704,16 @@ public class GocaDecoder {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSFLW: { // Set Fractional Line Width (0x11)
|
||||
if (orderLen >= 2 && idx + 1 < end) {
|
||||
byte[] flwData = new byte[orderLen - 1];
|
||||
System.arraycopy(inputData, idx + 1, flwData, 0, flwData.length);
|
||||
processFractionalLineWidth(flwData);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case 0x06:
|
||||
case 0x11:
|
||||
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
||||
lineType = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
@@ -661,8 +792,9 @@ public class GocaDecoder {
|
||||
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
||||
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||
boolean drawBoundary = (flags & 0x80) != 0;
|
||||
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary));
|
||||
beginArea(drawBoundary);
|
||||
int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
|
||||
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
|
||||
beginArea(drawBoundary, fillRule);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -770,7 +902,18 @@ public class GocaDecoder {
|
||||
int y = readCoord(inputData, idx + 4);
|
||||
int w = readCoord(inputData, idx + 6);
|
||||
int h = readCoord(inputData, idx + 8);
|
||||
beginImage(x, y, w, h);
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
if (payloadLen >= 9) {
|
||||
int fmt = inputData[idx + 10] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 10) {
|
||||
compression = inputData[idx + 11] & 0xFF;
|
||||
}
|
||||
beginImage(x, y, w, h, bitDepth, compression);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
@@ -887,16 +1030,21 @@ public class GocaDecoder {
|
||||
}
|
||||
|
||||
private void beginArea(boolean drawBoundary) {
|
||||
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
|
||||
}
|
||||
|
||||
private void beginArea(boolean drawBoundary, int fillRule) {
|
||||
this.inArea = true;
|
||||
this.areaDrawBoundary = drawBoundary;
|
||||
this.areaFillRule = fillRule;
|
||||
this.areaFill = true;
|
||||
this.fillColor = this.curColor;
|
||||
this.areaPointsX.clear();
|
||||
this.areaPointsY.clear();
|
||||
this.areaPolygons.clear();
|
||||
this.currentPolyPts = 0;
|
||||
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d",
|
||||
drawBoundary, fillColor, patternSet, pattern));
|
||||
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillRule=%d fillColor=0x%08x patternSet=%d pattern=%d",
|
||||
drawBoundary, fillRule, fillColor, patternSet, pattern));
|
||||
}
|
||||
|
||||
private void endArea() {
|
||||
@@ -928,11 +1076,11 @@ public class GocaDecoder {
|
||||
polyCounts[i] = areaPolygons.get(i);
|
||||
}
|
||||
|
||||
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d",
|
||||
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix));
|
||||
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d fillRule=%d",
|
||||
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix, areaFillRule));
|
||||
|
||||
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
|
||||
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
|
||||
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor, areaFillRule);
|
||||
inArea = false;
|
||||
areaPointsX.clear();
|
||||
areaPointsY.clear();
|
||||
@@ -971,11 +1119,17 @@ public class GocaDecoder {
|
||||
}
|
||||
|
||||
private void beginImage(int x, int y, int w, int h) {
|
||||
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
|
||||
}
|
||||
|
||||
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
|
||||
this.inImage = true;
|
||||
this.imgX = x;
|
||||
this.imgY = y;
|
||||
this.imgWidth = w;
|
||||
this.imgHeight = h;
|
||||
this.imgBitDepth = bitDepth;
|
||||
this.imgCompression = compression;
|
||||
this.imgBuffer.clear();
|
||||
}
|
||||
|
||||
@@ -991,7 +1145,7 @@ public class GocaDecoder {
|
||||
|
||||
int px = plane.mapX(imgX);
|
||||
int py = plane.mapY(imgY);
|
||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor);
|
||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
|
||||
inImage = false;
|
||||
imgBuffer.clear();
|
||||
}
|
||||
@@ -1328,7 +1482,7 @@ public class GocaDecoder {
|
||||
}
|
||||
String text = new String(chars);
|
||||
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
||||
curColor, cw, ch, charDir, charAngle);
|
||||
curColor, cw, ch, charDir, charAngle, charShear);
|
||||
} else if (charSet != 0 && programSymbolManager != null) {
|
||||
for (int i = 0; i < textLen; i++) {
|
||||
int code = data[pos + i] & 0xFF;
|
||||
@@ -1366,7 +1520,7 @@ public class GocaDecoder {
|
||||
}
|
||||
String text = new String(chars);
|
||||
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
||||
curColor, cw, ch, charDir, charAngle);
|
||||
curColor, cw, ch, charDir, charAngle, charShear);
|
||||
}
|
||||
|
||||
switch (charDir) {
|
||||
@@ -1395,11 +1549,15 @@ public class GocaDecoder {
|
||||
* Draws a transformed character string (matching HODDecoder.drawGCS).
|
||||
*/
|
||||
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) {
|
||||
drawGcs(x, y, text, color, cw, ch, dir, angle, 0.0);
|
||||
}
|
||||
|
||||
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle, double shearAngle) {
|
||||
if (plane != null && text != null && !text.isEmpty()) {
|
||||
if (charPrecision == GocaConstants.CP_STROKE) {
|
||||
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle);
|
||||
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle, shearAngle);
|
||||
} else {
|
||||
plane.drawText(x, y, text, color, cw, ch, dir, angle);
|
||||
plane.drawText(x, y, text, color, cw, ch, dir, angle, shearAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1408,12 +1566,16 @@ public class GocaDecoder {
|
||||
* Draws an EBCDIC byte buffer as a transformed character string.
|
||||
*/
|
||||
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) {
|
||||
drawGcs(ebcdicData, offset, length, color, cw, ch, dir, angle, 0.0);
|
||||
}
|
||||
|
||||
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle, double shearAngle) {
|
||||
if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
|
||||
char[] chars = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]);
|
||||
}
|
||||
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle);
|
||||
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle, shearAngle);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -106,4 +106,11 @@ public class GraphicInputBuilder {
|
||||
|
||||
return sf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field for pick correlation with aperture.
|
||||
*/
|
||||
public static byte[] buildPickCorrelation(int gocaX, int gocaY, int aperture) {
|
||||
return buildGraphicInput(gocaX, gocaY, 1, true, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ public class GraphicsPlane {
|
||||
this.canvasWidth = w;
|
||||
this.canvasHeight = h;
|
||||
this.rgbBuffer = newBuffer;
|
||||
updateViewingWindowPixels();
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
@@ -170,6 +171,59 @@ public class GraphicsPlane {
|
||||
return rgbBuffer;
|
||||
}
|
||||
|
||||
private int viewingWindowXMin = 0;
|
||||
private int viewingWindowYMin = 0;
|
||||
private int viewingWindowXMax = 0;
|
||||
private int viewingWindowYMax = 0;
|
||||
private boolean viewingWindowActive = false;
|
||||
private int clipPixelXMin = 0;
|
||||
private int clipPixelYMin = 0;
|
||||
private int clipPixelXMax = 0;
|
||||
private int clipPixelYMax = 0;
|
||||
private double fractionalLineWidth = 1.0;
|
||||
|
||||
public synchronized void setViewingWindow(int xMin, int yMin, int xMax, int yMax) {
|
||||
this.viewingWindowXMin = xMin;
|
||||
this.viewingWindowYMin = yMin;
|
||||
this.viewingWindowXMax = xMax;
|
||||
this.viewingWindowYMax = yMax;
|
||||
this.viewingWindowActive = true;
|
||||
updateViewingWindowPixels();
|
||||
}
|
||||
|
||||
public synchronized void clearViewingWindow() {
|
||||
this.viewingWindowActive = false;
|
||||
}
|
||||
|
||||
public synchronized boolean isViewingWindowActive() {
|
||||
return viewingWindowActive;
|
||||
}
|
||||
|
||||
public synchronized int getViewingWindowXMin() { return viewingWindowXMin; }
|
||||
public synchronized int getViewingWindowYMin() { return viewingWindowYMin; }
|
||||
public synchronized int getViewingWindowXMax() { return viewingWindowXMax; }
|
||||
public synchronized int getViewingWindowYMax() { return viewingWindowYMax; }
|
||||
|
||||
public synchronized void setFractionalLineWidth(double flw) {
|
||||
this.fractionalLineWidth = Math.max(0.1, flw);
|
||||
}
|
||||
|
||||
public synchronized double getFractionalLineWidth() {
|
||||
return fractionalLineWidth;
|
||||
}
|
||||
|
||||
private void updateViewingWindowPixels() {
|
||||
if (!viewingWindowActive) return;
|
||||
int px1 = mapX(viewingWindowXMin);
|
||||
int px2 = mapX(viewingWindowXMax);
|
||||
int py1 = mapY(viewingWindowYMin);
|
||||
int py2 = mapY(viewingWindowYMax);
|
||||
this.clipPixelXMin = Math.max(0, Math.min(px1, px2));
|
||||
this.clipPixelXMax = Math.min(canvasWidth - 1, Math.max(px1, px2));
|
||||
this.clipPixelYMin = Math.max(0, Math.min(py1, py2));
|
||||
this.clipPixelYMax = Math.min(canvasHeight - 1, Math.max(py1, py2));
|
||||
}
|
||||
|
||||
public int getCanvasWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
@@ -313,6 +367,11 @@ public class GraphicsPlane {
|
||||
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
|
||||
*/
|
||||
public synchronized void setPixel(int x, int y, int colorArgb) {
|
||||
if (viewingWindowActive) {
|
||||
if (x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||
int srcA = (colorArgb >>> 24) & 0xFF;
|
||||
if (srcA == 0) return;
|
||||
@@ -447,11 +506,15 @@ public class GraphicsPlane {
|
||||
|
||||
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
|
||||
if (brightness <= 0.0) return;
|
||||
if (lineWidth == GocaConstants.LW_THICK) {
|
||||
if (lineWidth == GocaConstants.LW_THICK || fractionalLineWidth >= 1.5) {
|
||||
int extra = (int) Math.round(Math.max(1, fractionalLineWidth - 0.5));
|
||||
setPixelCoverage(x, y, colorRgb, 1.0);
|
||||
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness));
|
||||
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
|
||||
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
|
||||
for (int dx = -extra; dx <= extra; dx++) {
|
||||
for (int dy = -extra; dy <= extra; dy++) {
|
||||
if (dx == 0 && dy == 0) continue;
|
||||
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Perceptual gamma correction for crisp contrast on dark backgrounds
|
||||
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
|
||||
@@ -641,6 +704,9 @@ public class GraphicsPlane {
|
||||
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
|
||||
*/
|
||||
/**
|
||||
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
|
||||
*/
|
||||
@@ -648,25 +714,41 @@ public class GraphicsPlane {
|
||||
int fillColorArgb, int patternSet, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb) {
|
||||
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, patternSet, pattern,
|
||||
drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, GocaConstants.FILL_RULE_EVEN_ODD);
|
||||
}
|
||||
|
||||
private static class NodeInter implements Comparable<NodeInter> {
|
||||
final int x;
|
||||
final int dir;
|
||||
|
||||
NodeInter(int x, int dir) {
|
||||
this.x = x;
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(NodeInter o) {
|
||||
return Integer.compare(this.x, o.x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
|
||||
int fillColorArgb, int patternSet, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb, int fillRule) {
|
||||
if (px == null || py == null || numPoints < 3) return;
|
||||
|
||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int bg = bgColorArgb;
|
||||
|
||||
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
|
||||
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
|
||||
// Background Mix (GSBMX / bgMix):
|
||||
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
|
||||
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
|
||||
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
|
||||
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
|
||||
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
|
||||
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
|
||||
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
|
||||
|
||||
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||
// Find polygon vertical bounds across all points
|
||||
int minY = py[0];
|
||||
int maxY = py[0];
|
||||
for (int i = 1; i < numPoints; i++) {
|
||||
@@ -676,7 +758,7 @@ public class GraphicsPlane {
|
||||
minY = Math.max(0, minY);
|
||||
maxY = Math.min(canvasHeight - 1, maxY);
|
||||
|
||||
List<Integer> nodeX = new ArrayList<>();
|
||||
List<NodeInter> nodeIntersections = new ArrayList<>();
|
||||
byte[] patRows = null;
|
||||
ProgramSymbolSet.SymbolSlot psSlot = null;
|
||||
if (patternSet >= 0x40 && programSymbolManager != null) {
|
||||
@@ -691,7 +773,7 @@ public class GraphicsPlane {
|
||||
}
|
||||
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
nodeX.clear();
|
||||
nodeIntersections.clear();
|
||||
int offset = 0;
|
||||
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
|
||||
for (int p = 0; p < polyCount; p++) {
|
||||
@@ -705,7 +787,8 @@ public class GraphicsPlane {
|
||||
int xj = px[offset + j];
|
||||
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
|
||||
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
|
||||
nodeX.add(x);
|
||||
int dir = (yj > yi) ? 1 : -1;
|
||||
nodeIntersections.add(new NodeInter(x, dir));
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
@@ -713,35 +796,72 @@ public class GraphicsPlane {
|
||||
offset += pLen;
|
||||
}
|
||||
|
||||
Collections.sort(nodeX);
|
||||
Collections.sort(nodeIntersections);
|
||||
|
||||
for (int i = 0; i < nodeX.size(); i += 2) {
|
||||
if (i + 1 >= nodeX.size()) break;
|
||||
int leftX = Math.max(0, nodeX.get(i));
|
||||
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
|
||||
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
|
||||
int winding = 0;
|
||||
for (int i = 0; i < nodeIntersections.size() - 1; i++) {
|
||||
winding += nodeIntersections.get(i).dir;
|
||||
if (winding != 0) {
|
||||
int leftX = Math.max(0, nodeIntersections.get(i).x);
|
||||
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
|
||||
|
||||
for (int x = leftX; x <= rightX; x++) {
|
||||
if (psSlot != null) {
|
||||
int psW = psSlot.getWidth();
|
||||
int psH = psSlot.getHeight();
|
||||
int psX = (psW > 0) ? (x % psW) : 0;
|
||||
int psY = (psH > 0) ? (y % psH) : 0;
|
||||
byte[] psPix = psSlot.getPixelData();
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
setPixel(x, y, bg);
|
||||
for (int x = leftX; x <= rightX; x++) {
|
||||
if (psSlot != null) {
|
||||
int psW = psSlot.getWidth();
|
||||
int psH = psSlot.getHeight();
|
||||
int psX = (psW > 0) ? (x % psW) : 0;
|
||||
int psY = (psH > 0) ? (y % psH) : 0;
|
||||
byte[] psPix = psSlot.getPixelData();
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
setPixel(x, y, bg);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
setPixel(x, y, fill);
|
||||
} else {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
setPixel(x, y, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
setPixel(x, y, fill);
|
||||
} else {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < nodeIntersections.size(); i += 2) {
|
||||
if (i + 1 >= nodeIntersections.size()) break;
|
||||
int leftX = Math.max(0, nodeIntersections.get(i).x);
|
||||
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
|
||||
|
||||
for (int x = leftX; x <= rightX; x++) {
|
||||
if (psSlot != null) {
|
||||
int psW = psSlot.getWidth();
|
||||
int psH = psSlot.getHeight();
|
||||
int psX = (psW > 0) ? (x % psW) : 0;
|
||||
int psY = (psH > 0) ? (y % psH) : 0;
|
||||
byte[] psPix = psSlot.getPixelData();
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
setPixel(x, y, bg);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background)
|
||||
setPixel(x, y, bg);
|
||||
} else {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
setPixel(x, y, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -857,19 +977,29 @@ public class GraphicsPlane {
|
||||
*/
|
||||
public synchronized void drawText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle) {
|
||||
drawText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
|
||||
}
|
||||
|
||||
public synchronized void drawText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
if (textRenderer != null) {
|
||||
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
} else {
|
||||
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
||||
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void drawText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
||||
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
|
||||
}
|
||||
|
||||
public synchronized void drawText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
|
||||
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -877,6 +1007,11 @@ public class GraphicsPlane {
|
||||
*/
|
||||
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle) {
|
||||
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
|
||||
}
|
||||
|
||||
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
|
||||
@@ -884,25 +1019,37 @@ public class GraphicsPlane {
|
||||
double ch = cellHeight > 0 ? cellHeight : 20.0;
|
||||
double curX = x;
|
||||
double curY = y;
|
||||
if (dir == GocaConstants.CD_TB) {
|
||||
curY += ch;
|
||||
} else if (dir == GocaConstants.CD_RL) {
|
||||
curX -= cw;
|
||||
|
||||
double radAngle = Math.toRadians(angle);
|
||||
double cosA = Math.cos(radAngle);
|
||||
double sinA = Math.sin(radAngle);
|
||||
|
||||
if (angle == 0.0) {
|
||||
if (dir == GocaConstants.CD_TB) {
|
||||
curY += ch;
|
||||
} else if (dir == GocaConstants.CD_RL) {
|
||||
curX -= cw;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
drawVssChar(curX, curY, c, color, cw, ch);
|
||||
drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
|
||||
|
||||
switch (dir) {
|
||||
case GocaConstants.CD_TB: curY += ch; break;
|
||||
case GocaConstants.CD_RL: curX -= cw; break;
|
||||
case GocaConstants.CD_BT: curY -= ch; break;
|
||||
case GocaConstants.CD_LR:
|
||||
case GocaConstants.CD_DEFAULT:
|
||||
default:
|
||||
curX += cw;
|
||||
break;
|
||||
if (angle != 0.0) {
|
||||
curX += cw * cosA;
|
||||
curY += cw * sinA;
|
||||
} else {
|
||||
switch (dir) {
|
||||
case GocaConstants.CD_TB: curY += ch; break;
|
||||
case GocaConstants.CD_RL: curX -= cw; break;
|
||||
case GocaConstants.CD_BT: curY -= ch; break;
|
||||
case GocaConstants.CD_LR:
|
||||
case GocaConstants.CD_DEFAULT:
|
||||
default:
|
||||
curX += cw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
@@ -911,10 +1058,19 @@ public class GraphicsPlane {
|
||||
|
||||
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
|
||||
}
|
||||
|
||||
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
|
||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
|
||||
}
|
||||
|
||||
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
||||
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
|
||||
}
|
||||
|
||||
private void drawVssChar(double x, double y, char c, int color, double cw, double ch, double angle, double shearAngle) {
|
||||
int code = (int) c;
|
||||
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
|
||||
return;
|
||||
@@ -924,6 +1080,11 @@ public class GraphicsPlane {
|
||||
return;
|
||||
}
|
||||
|
||||
double radAngle = Math.toRadians(angle);
|
||||
double cosA = Math.cos(radAngle);
|
||||
double sinA = Math.sin(radAngle);
|
||||
double tanShear = Math.tan(Math.toRadians(shearAngle));
|
||||
|
||||
int ptr = offset;
|
||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||
@@ -941,8 +1102,18 @@ public class GraphicsPlane {
|
||||
for (int p = 0; p < numPoints; p++) {
|
||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||
py[p] = y - ((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||
|
||||
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||
|
||||
double sx = nx - ny * tanShear;
|
||||
double sy = ny;
|
||||
|
||||
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
|
||||
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
|
||||
|
||||
px[p] = x + rx;
|
||||
py[p] = y + ry;
|
||||
ipx[p] = (int) Math.round(px[p]);
|
||||
ipy[p] = (int) Math.round(py[p]);
|
||||
}
|
||||
@@ -970,21 +1141,91 @@ public class GraphicsPlane {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws raw image pixel bitmap.
|
||||
* Draws raw image pixel bitmap with default 1-bit depth and uncompressed format.
|
||||
*/
|
||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
|
||||
drawImage(x, y, width, height, imageData, fgColorArgb, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws bitmap image with support for 1-bit, 2-bit, 4-bit, 8-bit depth and RLE decompression.
|
||||
*/
|
||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
|
||||
int bitDepth, int compressionMode) {
|
||||
if (imageData == null || width <= 0 || height <= 0) return;
|
||||
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int bytesPerRow = (width + 7) / 8;
|
||||
|
||||
for (int row = 0; row < height; row++) {
|
||||
int rowOffset = row * bytesPerRow;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int byteIdx = rowOffset + (col / 8);
|
||||
if (byteIdx < imageData.length) {
|
||||
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
||||
if (bit) {
|
||||
setPixel(x + col, y + row, fgColor);
|
||||
byte[] rawData = imageData;
|
||||
if (compressionMode == GocaConstants.IMG_RLE) {
|
||||
rawData = decompressGocaRle(imageData, width, height, bitDepth);
|
||||
}
|
||||
|
||||
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
|
||||
|
||||
if (depth == 1) {
|
||||
int bytesPerRow = (width + 7) / 8;
|
||||
for (int row = 0; row < height; row++) {
|
||||
int rowOffset = row * bytesPerRow;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int byteIdx = rowOffset + (col / 8);
|
||||
if (byteIdx < rawData.length) {
|
||||
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
||||
if (bit) {
|
||||
setPixel(x + col, y + row, fgColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (depth == 2) {
|
||||
int pixelsPerByte = 4;
|
||||
int bytesPerRow = (width + 3) / 4;
|
||||
for (int row = 0; row < height; row++) {
|
||||
int rowOffset = row * bytesPerRow;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int byteIdx = rowOffset + (col / pixelsPerByte);
|
||||
if (byteIdx < rawData.length) {
|
||||
int shift = (3 - (col % 4)) * 2;
|
||||
int val = (rawData[byteIdx] >> shift) & 0x03;
|
||||
if (val != 0) {
|
||||
int pixelColor;
|
||||
switch (val) {
|
||||
case 1: pixelColor = GocaConstants.GOCA_COLORS[1]; break; // Blue
|
||||
case 2: pixelColor = GocaConstants.GOCA_COLORS[2]; break; // Red
|
||||
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
|
||||
default: pixelColor = fgColor; break;
|
||||
}
|
||||
setPixel(x + col, y + row, pixelColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (depth == 4) {
|
||||
int pixelsPerByte = 2;
|
||||
int bytesPerRow = (width + 1) / 2;
|
||||
for (int row = 0; row < height; row++) {
|
||||
int rowOffset = row * bytesPerRow;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int byteIdx = rowOffset + (col / pixelsPerByte);
|
||||
if (byteIdx < rawData.length) {
|
||||
int shift = (1 - (col % 2)) * 4;
|
||||
int val = (rawData[byteIdx] >> shift) & 0x0F;
|
||||
if (val != 0) {
|
||||
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (depth == 8) {
|
||||
int bytesPerRow = width;
|
||||
for (int row = 0; row < height; row++) {
|
||||
int rowOffset = row * bytesPerRow;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int byteIdx = rowOffset + col;
|
||||
if (byteIdx < rawData.length) {
|
||||
int val = rawData[byteIdx] & 0xFF;
|
||||
if (val != 0) {
|
||||
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -992,4 +1233,35 @@ public class GraphicsPlane {
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompresses IBM GOCA Run-Length Encoded (RLE) bitmap raster streams.
|
||||
*/
|
||||
public static byte[] decompressGocaRle(byte[] rleData, int width, int height, int bitDepth) {
|
||||
if (rleData == null || rleData.length == 0) return new byte[0];
|
||||
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
|
||||
int bytesPerRow = (width * depth + 7) / 8;
|
||||
int expectedTotalBytes = bytesPerRow * height;
|
||||
byte[] out = new byte[expectedTotalBytes];
|
||||
int outIdx = 0;
|
||||
int inIdx = 0;
|
||||
|
||||
while (inIdx < rleData.length && outIdx < expectedTotalBytes) {
|
||||
int count = rleData[inIdx++] & 0xFF;
|
||||
if (count == 0) {
|
||||
if (inIdx < rleData.length) {
|
||||
int litLen = rleData[inIdx++] & 0xFF;
|
||||
for (int k = 0; k < litLen && inIdx < rleData.length && outIdx < expectedTotalBytes; k++) {
|
||||
out[outIdx++] = rleData[inIdx++];
|
||||
}
|
||||
}
|
||||
} else if (inIdx < rleData.length) {
|
||||
byte val = rleData[inIdx++];
|
||||
for (int k = 0; k < count && outIdx < expectedTotalBytes; k++) {
|
||||
out[outIdx++] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,15 +212,14 @@ public class ProgramSymbolManager {
|
||||
int remaining = data.length - offset;
|
||||
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||
|
||||
int bytesPerSymbol;
|
||||
if (loadFormat == 1) {
|
||||
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
|
||||
} else {
|
||||
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
|
||||
}
|
||||
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
|
||||
if (sliceBytes <= 0) sliceBytes = 18;
|
||||
|
||||
if (bytesPerSymbol <= 0) {
|
||||
bytesPerSymbol = 18;
|
||||
int bytesPerSymbol;
|
||||
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
|
||||
bytesPerSymbol = sliceBytes * 3;
|
||||
} else {
|
||||
bytesPerSymbol = sliceBytes;
|
||||
}
|
||||
|
||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||
@@ -230,10 +229,23 @@ public class ProgramSymbolManager {
|
||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
||||
}
|
||||
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
|
||||
// 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
||||
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
||||
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
||||
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
||||
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
||||
}
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
}
|
||||
}
|
||||
|
||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
package haus.nightmare.lib3270j.listener;
|
||||
|
||||
/**
|
||||
* Listener interface for receiving inbound SCS (SNA Character String) data streams
|
||||
* transmitted by the host in TN3270E mode (Data Type DT_SCS_DATA = 0x01).
|
||||
*/
|
||||
public interface SCSInboundListener {
|
||||
|
||||
/**
|
||||
* Called when an inbound SCS record is received from the host.
|
||||
*
|
||||
* @param data Raw SCS record payload bytes
|
||||
* @param offset Start offset within buffer
|
||||
* @param length Number of bytes in record
|
||||
*/
|
||||
void onSCSDataReceived(byte[] data, int offset, int length);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
@@ -17,7 +17,7 @@ import java.util.logging.Logger;
|
||||
/**
|
||||
* Network Virtual Terminal (NVT) processor.
|
||||
* Handles ASCII / ANSI VT100 character stream processing, cursor positioning,
|
||||
* escape sequence decoding, and NVT character/string transmission.
|
||||
* escape sequence decoding, terminal capability reports, and NVT transmission.
|
||||
*/
|
||||
public class NvtProcessor {
|
||||
|
||||
@@ -28,9 +28,10 @@ public class NvtProcessor {
|
||||
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Escape sequence parser states
|
||||
private static final int STATE_NORMAL = 0;
|
||||
private static final int STATE_ESC = 1;
|
||||
private static final int STATE_CSI = 2;
|
||||
private static final int STATE_NORMAL = 0;
|
||||
private static final int STATE_ESC = 1;
|
||||
private static final int STATE_CSI = 2;
|
||||
private static final int STATE_CHARSET = 3;
|
||||
|
||||
private int parseState = STATE_NORMAL;
|
||||
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
|
||||
@@ -47,6 +48,21 @@ public class NvtProcessor {
|
||||
private int savedCursorRow = 0;
|
||||
private int savedCursorCol = 0;
|
||||
|
||||
// Scrolling margins (0-indexed, inclusive)
|
||||
private int scrollTop = 0;
|
||||
private int scrollBottom = -1; // -1 means default (rows - 1)
|
||||
|
||||
// Tab stops
|
||||
private boolean[] tabStops;
|
||||
|
||||
// Cursor visibility
|
||||
private boolean cursorVisible = true;
|
||||
|
||||
// Line drawing mode
|
||||
private boolean lineDrawingG0 = false;
|
||||
private boolean lineDrawingG1 = false;
|
||||
private boolean activeCharsetG1 = false;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
void sendRaw(byte[] data) throws IOException;
|
||||
@@ -55,6 +71,15 @@ public class NvtProcessor {
|
||||
public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) {
|
||||
this.screenBuffer = screenBuffer;
|
||||
this.translator = translator;
|
||||
initTabStops();
|
||||
}
|
||||
|
||||
private void initTabStops() {
|
||||
int cols = screenBuffer.getCols();
|
||||
tabStops = new boolean[cols];
|
||||
for (int i = 0; i < cols; i++) {
|
||||
tabStops[i] = (i % 8 == 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOutputSender(OutputSender outputSender) {
|
||||
@@ -65,6 +90,14 @@ public class NvtProcessor {
|
||||
screenListeners.add(l);
|
||||
}
|
||||
|
||||
public void removeScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.remove(l);
|
||||
}
|
||||
|
||||
public boolean isCursorVisible() {
|
||||
return cursorVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process incoming ASCII NVT data bytes.
|
||||
*/
|
||||
@@ -73,6 +106,12 @@ public class NvtProcessor {
|
||||
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
if (tabStops == null || tabStops.length != cols) {
|
||||
initTabStops();
|
||||
}
|
||||
int effectiveScrollBottom = (scrollBottom >= 0 && scrollBottom < rows) ? scrollBottom : rows - 1;
|
||||
int effectiveScrollTop = Math.max(0, Math.min(scrollTop, effectiveScrollBottom));
|
||||
|
||||
int size = rows * cols;
|
||||
int curAddr = screenBuffer.getCursorAddress();
|
||||
|
||||
@@ -90,27 +129,60 @@ public class NvtProcessor {
|
||||
} else if (b == 0x0A) { // LF
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
r++;
|
||||
if (r >= rows) {
|
||||
scrollUp();
|
||||
r = rows - 1;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
} else if (b == 0x08 || b == 0x7F) { // BS or DEL
|
||||
} else if (b == 0x08) { // BS
|
||||
int c = curAddr % cols;
|
||||
if (c > 0) {
|
||||
curAddr--;
|
||||
}
|
||||
} else if (b == 0x7F) { // DEL
|
||||
// Ignore or backspace per NVT convention
|
||||
} else if (b == 0x09) { // TAB
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
int nextTab = ((c / 8) + 1) * 8;
|
||||
if (nextTab >= cols) nextTab = cols - 1;
|
||||
curAddr = (curAddr / cols) * cols + nextTab;
|
||||
int nextTab = cols - 1;
|
||||
for (int tc = c + 1; tc < cols; tc++) {
|
||||
if (tc < tabStops.length && tabStops[tc]) {
|
||||
nextTab = tc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
curAddr = r * cols + nextTab;
|
||||
} else if (b == 0x0C) { // FF
|
||||
screenBuffer.clear();
|
||||
curAddr = 0;
|
||||
} else if (b >= 0x20 && b < 0xFF) { // Printable ASCII
|
||||
} else if (b == 0x07) { // BEL
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onSoundAlarm();
|
||||
}
|
||||
} else if (b == 0x0E) { // SO (Select G1 charset)
|
||||
activeCharsetG1 = true;
|
||||
} else if (b == 0x0F) { // SI (Select G0 charset)
|
||||
activeCharsetG1 = false;
|
||||
} else if (b >= 0x20 && b <= 0xFF) { // Printable character
|
||||
char ch = (char) b;
|
||||
if (activeCharsetG1 ? lineDrawingG1 : lineDrawingG0) {
|
||||
ch = mapVt100SpecialGraphics(ch);
|
||||
}
|
||||
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
|
||||
if (c >= cols) {
|
||||
c = 0;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
}
|
||||
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
ExtendedAttribute cell = screenBuffer.getCell(curAddr);
|
||||
cell.clear();
|
||||
@@ -120,37 +192,98 @@ public class NvtProcessor {
|
||||
cell.bg = currentBg;
|
||||
cell.gr = currentGr;
|
||||
|
||||
curAddr++;
|
||||
if (curAddr >= size) {
|
||||
scrollUp();
|
||||
curAddr = (rows - 1) * cols;
|
||||
c++;
|
||||
if (c >= cols) {
|
||||
if (r < rows - 1) {
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
c = 0;
|
||||
} else {
|
||||
r++;
|
||||
c = 0;
|
||||
}
|
||||
} else {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
c = 0;
|
||||
}
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
}
|
||||
} else if (parseState == STATE_ESC) {
|
||||
escBuffer.write(b);
|
||||
if (b == '[') {
|
||||
parseState = STATE_CSI;
|
||||
} else if (b == '7') { // Save cursor
|
||||
} else if (b == '(' || b == ')') {
|
||||
parseState = STATE_CHARSET;
|
||||
} else if (b == '7') { // DECSC - Save cursor
|
||||
savedCursorRow = curAddr / cols;
|
||||
savedCursorCol = curAddr % cols;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == '8') { // Restore cursor
|
||||
} else if (b == '8') { // DECRC - Restore cursor
|
||||
curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol);
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'D') { // IND - Index (down 1 line)
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'M') { // RI - Reverse Index (up 1 line)
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
if (r == effectiveScrollTop) {
|
||||
scrollDownRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r > 0) {
|
||||
r--;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'E') { // NEL - Next Line (CR + LF)
|
||||
int r = curAddr / cols;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'H') { // HTS - Horizontal Tab Set
|
||||
int c = curAddr % cols;
|
||||
if (c < tabStops.length) {
|
||||
tabStops[c] = true;
|
||||
}
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'c') { // RIS - Reset to Initial State
|
||||
screenBuffer.clear();
|
||||
curAddr = 0;
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
scrollTop = 0;
|
||||
scrollBottom = rows - 1;
|
||||
cursorVisible = true;
|
||||
initTabStops();
|
||||
parseState = STATE_NORMAL;
|
||||
} else {
|
||||
// Unknown 2-byte escape, finish
|
||||
// Unknown 2-byte escape, return to normal
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
} else if (parseState == STATE_CHARSET) {
|
||||
byte[] seq = escBuffer.toByteArray();
|
||||
if (seq.length >= 2) {
|
||||
boolean isG1 = (seq[1] == ')');
|
||||
boolean isLineDraw = (b == '0');
|
||||
if (isG1) lineDrawingG1 = isLineDraw;
|
||||
else lineDrawingG0 = isLineDraw;
|
||||
}
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (parseState == STATE_CSI) {
|
||||
escBuffer.write(b);
|
||||
// CSI parameter/intermediate bytes: 0x20..0x3F, final bytes: 0x40..0x7E
|
||||
// CSI final bytes are in the range 0x40..0x7E
|
||||
if (b >= 0x40 && b <= 0x7E) {
|
||||
byte[] seq = escBuffer.toByteArray();
|
||||
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
||||
@@ -159,7 +292,7 @@ public class NvtProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
screenBuffer.setCursorAddress(curAddr);
|
||||
screenBuffer.setCursorAddress(Math.max(0, Math.min(size - 1, curAddr)));
|
||||
screenBuffer.markAllChanged();
|
||||
screenBuffer.updateDisplaySnapshot();
|
||||
notifyScreenUpdated();
|
||||
@@ -175,6 +308,9 @@ public class NvtProcessor {
|
||||
String paramStr = new String(seq, 2, seq.length - 3, StandardCharsets.US_ASCII);
|
||||
String[] params = paramStr.split(";");
|
||||
|
||||
int effectiveScrollBottom = (scrollBottom >= 0 && scrollBottom < rows) ? scrollBottom : rows - 1;
|
||||
int effectiveScrollTop = Math.max(0, Math.min(scrollTop, effectiveScrollBottom));
|
||||
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
|
||||
@@ -212,6 +348,31 @@ public class NvtProcessor {
|
||||
c = Math.max(0, c - count);
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'E': // CNL - Cursor Next Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
r = Math.min(rows - 1, r + count);
|
||||
return r * cols; // column 0
|
||||
}
|
||||
case 'F': // CPL - Cursor Previous Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
r = Math.max(0, r - count);
|
||||
return r * cols; // column 0
|
||||
}
|
||||
case 'G': // CHA - Cursor Horizontal Absolute
|
||||
case '`': // HPA - Horizontal Position Absolute
|
||||
{
|
||||
int p = parseParam(params, 0, 1) - 1;
|
||||
c = Math.max(0, Math.min(cols - 1, p));
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'd': // VPA - Vertical Position Absolute
|
||||
{
|
||||
int p = parseParam(params, 0, 1) - 1;
|
||||
r = Math.max(0, Math.min(rows - 1, p));
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'J': // ED - Erase in Display
|
||||
{
|
||||
int mode = parseParam(params, 0, 0);
|
||||
@@ -239,6 +400,84 @@ public class NvtProcessor {
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'L': // IL - Insert Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollDownRegion(r, effectiveScrollBottom);
|
||||
}
|
||||
return r * cols;
|
||||
}
|
||||
case 'M': // DL - Delete Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollUpRegion(r, effectiveScrollBottom);
|
||||
}
|
||||
return r * cols;
|
||||
}
|
||||
case '@': // ICH - Insert Character
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
int lineStart = r * cols;
|
||||
for (int col = cols - 1; col >= c + count; col--) {
|
||||
screenBuffer.getCell(lineStart + col).copyFrom(screenBuffer.getCell(lineStart + col - count));
|
||||
}
|
||||
for (int col = c; col < Math.min(cols, c + count); col++) {
|
||||
clearCell(lineStart + col);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'P': // DCH - Delete Character
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
int lineStart = r * cols;
|
||||
for (int col = c; col < cols - count; col++) {
|
||||
screenBuffer.getCell(lineStart + col).copyFrom(screenBuffer.getCell(lineStart + col + count));
|
||||
}
|
||||
for (int col = cols - count; col < cols; col++) {
|
||||
clearCell(lineStart + col);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'X': // ECH - Erase Character
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
int end = Math.min((r + 1) * cols, curAddr + count);
|
||||
for (int i = curAddr; i < end; i++) {
|
||||
clearCell(i);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'S': // SU - Scroll Up
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'T': // SD - Scroll Down
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollDownRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'r': // DECSTBM - Set Top and Bottom Margins (Scrolling Region)
|
||||
{
|
||||
int top = parseParam(params, 0, 1) - 1;
|
||||
int bottom = parseParam(params, 1, rows) - 1;
|
||||
if (top >= 0 && bottom < rows && top < bottom) {
|
||||
scrollTop = top;
|
||||
scrollBottom = bottom;
|
||||
} else {
|
||||
scrollTop = 0;
|
||||
scrollBottom = rows - 1;
|
||||
}
|
||||
return 0; // Move cursor to home
|
||||
}
|
||||
case 'm': // SGR - Select Graphic Rendition
|
||||
{
|
||||
if (params.length == 0 || (params.length == 1 && params[0].isEmpty())) {
|
||||
@@ -256,6 +495,57 @@ public class NvtProcessor {
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'n': // DSR - Device Status Report
|
||||
{
|
||||
int code = parseParam(params, 0, 0);
|
||||
if (code == 6) { // Cursor position request
|
||||
// Reply: ESC [ <row> ; <col> R (1-indexed)
|
||||
String response = String.format("\u001B[%d;%dR", r + 1, c + 1);
|
||||
sendResponseString(response);
|
||||
} else if (code == 5) { // Status report request
|
||||
sendResponseString("\u001B[0n"); // OK
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'c': // DA - Device Attributes
|
||||
{
|
||||
int code = parseParam(params, 0, 0);
|
||||
if (code == 0) {
|
||||
// Identify as standard VT100 with Advanced Video Option
|
||||
sendResponseString("\u001B[?1;2c");
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'g': // TBC - Tab Clear
|
||||
{
|
||||
int mode = parseParam(params, 0, 0);
|
||||
if (mode == 0) {
|
||||
if (c < tabStops.length) tabStops[c] = false;
|
||||
} else if (mode == 3) {
|
||||
Arrays.fill(tabStops, false);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'h': // Set Mode / Private Mode
|
||||
{
|
||||
if (paramStr.startsWith("?")) {
|
||||
String sub = paramStr.substring(1).trim();
|
||||
if ("25".equals(sub)) {
|
||||
cursorVisible = true;
|
||||
}
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'l': // Reset Mode / Private Mode
|
||||
{
|
||||
if (paramStr.startsWith("?")) {
|
||||
String sub = paramStr.substring(1).trim();
|
||||
if ("25".equals(sub)) {
|
||||
cursorVisible = false;
|
||||
}
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 's': // Save cursor
|
||||
savedCursorRow = r;
|
||||
savedCursorCol = c;
|
||||
@@ -269,10 +559,22 @@ public class NvtProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResponseString(String s) {
|
||||
if (outputSender != null) {
|
||||
try {
|
||||
outputSender.sendRaw(s.getBytes(StandardCharsets.US_ASCII));
|
||||
} catch (IOException e) {
|
||||
log.warning("Failed to send ANSI response: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int parseParam(String[] params, int idx, int defaultVal) {
|
||||
if (params != null && idx < params.length && !params[idx].trim().isEmpty()) {
|
||||
try {
|
||||
return Integer.parseInt(params[idx].trim());
|
||||
String val = params[idx].trim();
|
||||
if (val.startsWith("?")) val = val.substring(1);
|
||||
return Integer.parseInt(val);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return defaultVal;
|
||||
@@ -281,27 +583,73 @@ public class NvtProcessor {
|
||||
private void clearCell(int addr) {
|
||||
ExtendedAttribute cell = screenBuffer.getCell(addr);
|
||||
cell.clear();
|
||||
cell.ec = 0;
|
||||
cell.ec = (byte) 0x40; // EBCDIC space
|
||||
cell.ucs4 = ' ';
|
||||
cell.fg = 0;
|
||||
cell.bg = 0;
|
||||
cell.gr = 0;
|
||||
}
|
||||
|
||||
private void scrollUp() {
|
||||
private void scrollUpRegion(int top, int bottom) {
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
for (int r = 0; r < rows - 1; r++) {
|
||||
top = Math.max(0, Math.min(rows - 1, top));
|
||||
bottom = Math.max(top, Math.min(rows - 1, bottom));
|
||||
|
||||
for (int r = top; r < bottom; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int dst = r * cols + c;
|
||||
int src = (r + 1) * cols + c;
|
||||
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
|
||||
}
|
||||
}
|
||||
// Clear last line
|
||||
int lastRowStart = (rows - 1) * cols;
|
||||
int lastRowStart = bottom * cols;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
clearCell(lastRowStart + c);
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollDownRegion(int top, int bottom) {
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
top = Math.max(0, Math.min(rows - 1, top));
|
||||
bottom = Math.max(top, Math.min(rows - 1, bottom));
|
||||
|
||||
for (int r = bottom; r > top; r--) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int dst = r * cols + c;
|
||||
int src = (r - 1) * cols + c;
|
||||
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
|
||||
}
|
||||
}
|
||||
int topRowStart = top * cols;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
clearCell(topRowStart + c);
|
||||
}
|
||||
}
|
||||
|
||||
private char mapVt100SpecialGraphics(char c) {
|
||||
switch (c) {
|
||||
case 'j': return '┘';
|
||||
case 'k': return '┐';
|
||||
case 'l': return '┌';
|
||||
case 'm': return '└';
|
||||
case 'n': return '┼';
|
||||
case 'q': return '─';
|
||||
case 't': return '├';
|
||||
case 'u': return '┤';
|
||||
case 'v': return '┴';
|
||||
case 'w': return '┬';
|
||||
case 'x': return '│';
|
||||
case '`': return '◆';
|
||||
case 'a': return '▒';
|
||||
case 'f': return '°';
|
||||
case 'g': return '±';
|
||||
case '~': return '•';
|
||||
default: return c;
|
||||
}
|
||||
}
|
||||
|
||||
private void applySgr(int code) {
|
||||
switch (code) {
|
||||
case 0: // Reset
|
||||
|
||||
@@ -204,44 +204,52 @@ public final class DS3270Constants {
|
||||
public static final int SF_DESTROY_PART = 0x0d;
|
||||
public static final int SF_ACTIVATE_PART = 0x0e;
|
||||
public static final int SF_MODIFY_PART = 0x0f;
|
||||
public static final int SF_SET_WINDOW = 0x0f; // Set Window (GOCA/Modify Partition)
|
||||
public static final int SF_3270_GRAPHICS = 0x20; // 3270 Graphics / Object Control
|
||||
public static final int SF_OBJECT_CONTROL = 0x20; // Object Control
|
||||
public static final int SF_DOCUMENT_DATA = 0x24; // Document Data (embedded SCS / GOCA)
|
||||
public static final int SF_DOC_DATA = 0x24; // Alias for Document Data
|
||||
public static final int SF_OUTBOUND_DS = 0x40;
|
||||
public static final int SF_TRANSFER_DATA = 0xd0;
|
||||
|
||||
// ========== Query Reply codes ==========
|
||||
public static final int QR_SUMMARY = 0x80; // Summary
|
||||
public static final int QR_USABLE_AREA = 0x81; // Usable Area
|
||||
public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
|
||||
public static final int QR_TEXT_PART = 0x83; // Text Partitions
|
||||
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
|
||||
public static final int QR_CHARSETS = 0x85; // Character Sets
|
||||
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
|
||||
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
|
||||
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
|
||||
public static final int QR_OUTLINING = 0x8c; // Field Outlining
|
||||
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
|
||||
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
|
||||
public static final int QR_DDM = 0x95; // Distributed Data Management
|
||||
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
|
||||
public static final int QR_FILE = 0x9f; // File Transfer
|
||||
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
|
||||
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
|
||||
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
|
||||
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
|
||||
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
|
||||
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
|
||||
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
|
||||
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
|
||||
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
|
||||
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
|
||||
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
|
||||
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
|
||||
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
|
||||
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
|
||||
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
|
||||
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
|
||||
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
|
||||
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
|
||||
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
|
||||
public static final int QR_SUMMARY = 0x80; // Summary
|
||||
public static final int QR_USABLE_AREA = 0x81; // Usable Area
|
||||
public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
|
||||
public static final int QR_TEXT_PART = 0x83; // Text Partitions
|
||||
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
|
||||
public static final int QR_CHARSETS = 0x85; // Character Sets
|
||||
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
|
||||
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
|
||||
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
|
||||
public static final int QR_OUTLINING = 0x8c; // Field Outlining
|
||||
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
|
||||
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
|
||||
public static final int QR_DDM = 0x95; // Distributed Data Management
|
||||
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
|
||||
public static final int QR_FILE = 0x9f; // File Transfer
|
||||
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
|
||||
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
|
||||
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
|
||||
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
|
||||
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
|
||||
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
|
||||
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
|
||||
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
|
||||
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
|
||||
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
|
||||
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
|
||||
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
|
||||
public static final int QR_AUX_DEVICE = 0xb2; // Aux Device alias
|
||||
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
|
||||
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
|
||||
public static final int QR_OEM_FORMAT = 0xb3; // OEM Format alias
|
||||
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
|
||||
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
|
||||
public static final int QR_GRAPHIC_COLOR = 0xb4; // Graphic Color alias
|
||||
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
|
||||
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
|
||||
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
|
||||
|
||||
// ========== Screen model sizes ==========
|
||||
public static final int MODEL_2_ROWS = 24;
|
||||
|
||||
@@ -115,4 +115,23 @@ public final class TelnetConstants {
|
||||
default: return "CMD-" + cmd;
|
||||
}
|
||||
}
|
||||
public static String qualifierName(int qual) {
|
||||
switch (qual) {
|
||||
case TELQUAL_IS: return "IS";
|
||||
case TELQUAL_SEND: return "SEND";
|
||||
case TELQUAL_INFO: return "INFO";
|
||||
default: return "QUAL-" + qual;
|
||||
}
|
||||
}
|
||||
|
||||
/** NEW-ENVIRON object name lookup. */
|
||||
public static String environObjectName(int obj) {
|
||||
switch (obj) {
|
||||
case TELOBJ_VAR: return "VAR";
|
||||
case TELOBJ_VALUE: return "VALUE";
|
||||
case TELOBJ_ESC: return "ESC";
|
||||
case TELOBJ_USERVAR: return "USERVAR";
|
||||
default: return "OBJ-" + obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,20 @@ public class ExtendedAttribute {
|
||||
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */
|
||||
public byte bg;
|
||||
|
||||
// Character set constants
|
||||
public static final byte CS_BASE = 0;
|
||||
public static final byte CS_APL = 1;
|
||||
public static final byte CS_LINEDRAW = 2;
|
||||
public static final byte CS_DBCS = 3;
|
||||
public static final byte CS_GE = 0x04;
|
||||
|
||||
// DBCS state constants
|
||||
public static final byte DB_NONE = 0;
|
||||
public static final byte DB_LEFT = 1; // Left / first half of double-byte char
|
||||
public static final byte DB_RIGHT = 2; // Right / second half of double-byte char
|
||||
public static final byte DB_SI = 3; // Shift-In control char
|
||||
public static final byte DB_SO = 4; // Shift-Out control char
|
||||
|
||||
/**
|
||||
* Graphics rendition bits.
|
||||
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
|
||||
|
||||
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
@@ -39,6 +42,14 @@ public class ScreenBuffer {
|
||||
private byte defaultGr = 0x00;
|
||||
private byte defaultCs = 0x00;
|
||||
private byte defaultIc = 0x00;
|
||||
|
||||
// Entry Assist / DOC mode state
|
||||
private boolean docMode = false;
|
||||
private boolean wordWrap = false;
|
||||
private int docStartCol = 0;
|
||||
private int docEndCol = -1;
|
||||
private int[] tabStops = null;
|
||||
|
||||
private final EbcdicTranslator translator;
|
||||
private final Object renderLock = new Object();
|
||||
|
||||
@@ -216,6 +227,10 @@ public class ScreenBuffer {
|
||||
this.explicitPartitionActive = (pid != 0);
|
||||
}
|
||||
|
||||
public synchronized PartitionInfo getPartition(int pid) {
|
||||
return partitions.get(pid);
|
||||
}
|
||||
|
||||
public synchronized void eraseReset(boolean alt) {
|
||||
partitions.clear();
|
||||
this.activePartition = 0;
|
||||
@@ -269,7 +284,7 @@ public class ScreenBuffer {
|
||||
public void setFieldAttribute(int pos, byte fa) {
|
||||
ExtendedAttribute ea = buffer[pos];
|
||||
ea.clear();
|
||||
ea.fa = fa;
|
||||
ea.fa = (fa != 0) ? fa : (byte) FA_PRINTABLE;
|
||||
if (!formatted) {
|
||||
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
|
||||
}
|
||||
@@ -471,39 +486,10 @@ public class ScreenBuffer {
|
||||
}
|
||||
|
||||
private char getAplGraphic(int ec) {
|
||||
switch (ec) {
|
||||
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
|
||||
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
|
||||
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
|
||||
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
|
||||
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
|
||||
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
|
||||
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
|
||||
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
|
||||
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
|
||||
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
|
||||
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
|
||||
case 0xCB: return '\u253C'; // Cross -> '┼'
|
||||
|
||||
// Special math and APL symbols (matching x3270 cg.c / apl.c)
|
||||
case 0x8C: return '\u2264'; // Less-than or equal '≤'
|
||||
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
|
||||
case 0xBE: return '\u2260'; // Not equal '≠'
|
||||
case 0xAD: return '['; // Left bracket
|
||||
case 0xBD: return ']'; // Right bracket
|
||||
case 0x8D: return '{'; // Left brace
|
||||
case 0x9D: return '}'; // Right brace
|
||||
case 0xB0: return '\u00B0'; // Degree '°'
|
||||
case 0xB1: return '\u00B1'; // Plus-minus '±'
|
||||
case 0xB2: return '\u00B2'; // Superscript 2 '²'
|
||||
case 0xB3: return '\u00B3'; // Superscript 3 '³'
|
||||
case 0xAF: return '\u00AF'; // Overbar '¯'
|
||||
case 0xBA: return '\u03A9'; // Omega 'Ω'
|
||||
case 0xBF: return '\u00B5'; // Micro 'µ'
|
||||
case 0x5F: return '\u00AC'; // Not sign '¬'
|
||||
|
||||
default: return translator.ebcdicToUnicode(ec);
|
||||
if (translator != null) {
|
||||
return translator.mapAPL(ec);
|
||||
}
|
||||
return (char) (ec & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -567,4 +553,511 @@ public class ScreenBuffer {
|
||||
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
|
||||
return buffer[baddr].fa;
|
||||
}
|
||||
|
||||
// ========== Phase 3: Field Management & Navigation ==========
|
||||
|
||||
/**
|
||||
* Construct an ECLFieldList representation of the presentation space.
|
||||
*/
|
||||
public synchronized ECLFieldList buildFieldList() {
|
||||
return new ECLFieldList(new ECLPS(this, null, translator), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the ECLField at the specified row and column.
|
||||
*/
|
||||
public ECLField findFieldAt(int row, int col) {
|
||||
return buildFieldList().findFieldAt(row, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the ECLField containing the specified buffer position.
|
||||
*/
|
||||
public ECLField findFieldAt(int pos) {
|
||||
return buildFieldList().findFieldAt(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the ECLField containing the specified buffer position.
|
||||
*/
|
||||
public ECLField findField(int pos) {
|
||||
return buildFieldList().findField(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field preceding the field at the given position.
|
||||
*/
|
||||
public ECLField findPrevField(int pos) {
|
||||
return buildFieldList().findPrevField(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the field succeeding the field at the given position.
|
||||
*/
|
||||
public ECLField findNextField(int pos) {
|
||||
return buildFieldList().findNextField(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first field in the presentation space.
|
||||
*/
|
||||
public ECLField getFirstField() {
|
||||
return buildFieldList().getFirstField();
|
||||
}
|
||||
|
||||
// ========== Presentation Space Accessors & Convenience Methods ==========
|
||||
|
||||
public int getSize() {
|
||||
return rows * cols;
|
||||
}
|
||||
|
||||
public synchronized char getChar(int row, int col) {
|
||||
int addr = rowColToAddress(row, col);
|
||||
if (addr < 0 || addr >= rows * cols) return ' ';
|
||||
ExtendedAttribute ea = buffer[addr];
|
||||
if (ea.isFieldAttribute()) return ' ';
|
||||
if (ea.ucs4 != 0) return (char) ea.ucs4;
|
||||
if (ea.ec != 0) return translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||
return ' ';
|
||||
}
|
||||
|
||||
public synchronized void setChar(int row, int col, char c) {
|
||||
int addr = rowColToAddress(row, col);
|
||||
if (addr < 0 || addr >= rows * cols) return;
|
||||
int ebc = translator.unicodeToEbcdic(c);
|
||||
buffer[addr].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||
buffer[addr].ucs4 = c;
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
public synchronized void writeChar(int pos, byte ebc) {
|
||||
if (pos < 0 || pos >= rows * cols) return;
|
||||
buffer[pos].ec = ebc;
|
||||
buffer[pos].ucs4 = translator.ebcdicToUnicode(ebc & 0xFF);
|
||||
screenChanged = true;
|
||||
}
|
||||
|
||||
public byte getAttr(int row, int col) {
|
||||
return getFieldAttributeAt(rowColToAddress(row, col));
|
||||
}
|
||||
|
||||
public ExtendedAttribute getExtAttr(int row, int col) {
|
||||
return getCell(rowColToAddress(row, col));
|
||||
}
|
||||
|
||||
public synchronized void setExtAttr(int row, int col, ExtendedAttribute ea) {
|
||||
int addr = rowColToAddress(row, col);
|
||||
if (addr >= 0 && addr < rows * cols && ea != null) {
|
||||
buffer[addr].copyFrom(ea);
|
||||
screenChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized String getText() {
|
||||
int size = rows * cols;
|
||||
char[] buf = new char[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = buffer[i];
|
||||
if (ea.isFieldAttribute()) {
|
||||
buf[i] = ' ';
|
||||
} else if (ea.ucs4 != 0) {
|
||||
buf[i] = (char) ea.ucs4;
|
||||
} else if (ea.ec != 0) {
|
||||
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||
} else {
|
||||
buf[i] = ' ';
|
||||
}
|
||||
}
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
public synchronized String getString(int pos, int len) {
|
||||
if (len <= 0) return "";
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return "";
|
||||
char[] buf = new char[len];
|
||||
for (int i = 0; i < len; i++) {
|
||||
int addr = (pos + i) % size;
|
||||
ExtendedAttribute ea = buffer[addr];
|
||||
if (ea.isFieldAttribute()) {
|
||||
buf[i] = ' ';
|
||||
} else if (ea.ucs4 != 0) {
|
||||
buf[i] = (char) ea.ucs4;
|
||||
} else if (ea.ec != 0) {
|
||||
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||
} else {
|
||||
buf[i] = ' ';
|
||||
}
|
||||
}
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
public synchronized void setText(String text) {
|
||||
if (text == null) return;
|
||||
int size = rows * cols;
|
||||
int len = Math.min(text.length(), size);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = text.charAt(i);
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||
buffer[i].ucs4 = ch;
|
||||
}
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public int searchString(String target) {
|
||||
if (target == null || target.isEmpty()) return -1;
|
||||
String full = getText();
|
||||
return full.indexOf(target);
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
int size = rows * cols;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (buffer[i].isFieldAttribute() && faIsModified(buffer[i].fa & 0xFF)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isModified(int pos) {
|
||||
return faIsModified(getFieldAttributeAt(pos) & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isProtected(int pos) {
|
||||
return faIsProtected(getFieldAttributeAt(pos) & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isNumeric(int pos) {
|
||||
return faIsNumeric(getFieldAttributeAt(pos) & 0xFF);
|
||||
}
|
||||
|
||||
public boolean isDisplay(int pos) {
|
||||
return !faIsZero(getFieldAttributeAt(pos) & 0xFF);
|
||||
}
|
||||
|
||||
// ========== DBCS Character Insertion & Deletion ==========
|
||||
|
||||
/**
|
||||
* Insert a character at the specified buffer address with field boundary and DBCS preservation.
|
||||
*/
|
||||
public synchronized boolean insertChar(int pos, char ch) {
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
|
||||
if (formatted) {
|
||||
byte faVal = getFieldAttributeAt(pos);
|
||||
if (faIsProtected(faVal & 0xFF)) return false;
|
||||
}
|
||||
|
||||
boolean isDbcsChar = translator != null && translator.isDBCS() && translator.unicodeToDbcs(ch) >= 0;
|
||||
int shiftAmount = isDbcsChar ? 2 : 1;
|
||||
|
||||
// Find end of field
|
||||
int endAddr = pos;
|
||||
int count = 0;
|
||||
while (!getCell(incrementAddress(endAddr)).isFieldAttribute() && count < size) {
|
||||
endAddr = incrementAddress(endAddr);
|
||||
count++;
|
||||
if (endAddr == pos) break;
|
||||
}
|
||||
|
||||
// Check overflow
|
||||
for (int s = 0; s < shiftAmount; s++) {
|
||||
int checkAddr = endAddr;
|
||||
for (int k = 0; k < s; k++) checkAddr = decrementAddress(checkAddr);
|
||||
ExtendedAttribute eaEnd = getCell(checkAddr);
|
||||
if (eaEnd.ec != 0 && eaEnd.ec != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Shift characters right
|
||||
for (int s = 0; s < shiftAmount; s++) {
|
||||
int dst = endAddr;
|
||||
int shiftCount = 0;
|
||||
while (dst != pos && shiftCount < size) {
|
||||
int src = decrementAddress(dst);
|
||||
getCell(dst).copyFrom(getCell(src));
|
||||
dst = src;
|
||||
shiftCount++;
|
||||
}
|
||||
getCell(pos).clear();
|
||||
}
|
||||
|
||||
if (isDbcsChar) {
|
||||
int dbcs = translator.unicodeToDbcs(ch);
|
||||
int b1 = (dbcs >> 8) & 0xFF;
|
||||
int b2 = dbcs & 0xFF;
|
||||
int nextPos = incrementAddress(pos);
|
||||
|
||||
ExtendedAttribute ea1 = getCell(pos);
|
||||
ea1.ec = (byte) b1;
|
||||
ea1.ucs4 = ch;
|
||||
ea1.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea1.db = ExtendedAttribute.DB_LEFT;
|
||||
|
||||
ExtendedAttribute ea2 = getCell(nextPos);
|
||||
ea2.ec = (byte) b2;
|
||||
ea2.ucs4 = ch;
|
||||
ea2.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea2.db = ExtendedAttribute.DB_RIGHT;
|
||||
|
||||
setCursorAddress(incrementAddress(nextPos));
|
||||
} else {
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
ExtendedAttribute ea = getCell(pos);
|
||||
ea.ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||
ea.ucs4 = ch;
|
||||
ea.cs = ExtendedAttribute.CS_BASE;
|
||||
ea.db = ExtendedAttribute.DB_NONE;
|
||||
|
||||
setCursorAddress(incrementAddress(pos));
|
||||
}
|
||||
|
||||
// Set MDT
|
||||
if (formatted) {
|
||||
int faAddr = findFieldAttribute(pos);
|
||||
if (faAddr >= 0) {
|
||||
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
}
|
||||
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean insertChar(char ch) {
|
||||
return insertChar(cursorAddress, ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a character at the specified buffer address, pulling trailing field text and preserving DBCS glyphs.
|
||||
*/
|
||||
public synchronized boolean deleteChar(int pos) {
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return false;
|
||||
pos = ((pos % size) + size) % size;
|
||||
|
||||
if (getCell(pos).isFieldAttribute()) return false;
|
||||
if (formatted) {
|
||||
byte faVal = getFieldAttributeAt(pos);
|
||||
if (faIsProtected(faVal & 0xFF)) return false;
|
||||
}
|
||||
|
||||
ExtendedAttribute curCell = getCell(pos);
|
||||
boolean isDbcs = curCell.db == ExtendedAttribute.DB_LEFT || curCell.db == ExtendedAttribute.DB_RIGHT
|
||||
|| curCell.cs == ExtendedAttribute.CS_DBCS;
|
||||
int deleteAmount = isDbcs ? 2 : 1;
|
||||
if (curCell.db == ExtendedAttribute.DB_RIGHT) {
|
||||
pos = decrementAddress(pos);
|
||||
}
|
||||
|
||||
for (int d = 0; d < deleteAmount; d++) {
|
||||
int shiftAddr = pos;
|
||||
int count = 0;
|
||||
while (count < size) {
|
||||
int next = incrementAddress(shiftAddr);
|
||||
if (getCell(next).isFieldAttribute()) {
|
||||
getCell(shiftAddr).clear();
|
||||
break;
|
||||
}
|
||||
getCell(shiftAddr).copyFrom(getCell(next));
|
||||
shiftAddr = next;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
cleanAdjacentSISO(pos);
|
||||
|
||||
if (formatted) {
|
||||
int faAddr = findFieldAttribute(pos);
|
||||
if (faAddr >= 0) {
|
||||
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
}
|
||||
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean deleteChar() {
|
||||
return deleteChar(cursorAddress);
|
||||
}
|
||||
|
||||
private void cleanAdjacentSISO(int nearPos) {
|
||||
int size = rows * cols;
|
||||
int start = Math.max(0, nearPos - 5);
|
||||
int end = Math.min(size, nearPos + 10);
|
||||
for (int i = start; i < end - 1; i++) {
|
||||
ExtendedAttribute ea1 = getCell(i);
|
||||
ExtendedAttribute ea2 = getCell(i + 1);
|
||||
if (!ea1.isFieldAttribute() && !ea2.isFieldAttribute()) {
|
||||
if ((ea1.ec & 0xFF) == 0x0E && (ea2.ec & 0xFF) == 0x0F) {
|
||||
ea1.clear();
|
||||
ea2.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Entry Assist & DOC Mode Operations ==========
|
||||
|
||||
public boolean isEntryAssistDOCmode() { return docMode; }
|
||||
public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; }
|
||||
|
||||
public boolean isEntryAssistWordWrap() { return wordWrap; }
|
||||
public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; }
|
||||
|
||||
public int getEntryAssistStartColumn() { return docStartCol; }
|
||||
public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); }
|
||||
|
||||
public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); }
|
||||
public void setEntryAssistEndColumn(int n) { this.docEndCol = n; }
|
||||
|
||||
public int[] getEntryAssistTabStops() { return tabStops; }
|
||||
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
|
||||
|
||||
public synchronized void processWordTab(boolean forward) {
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return;
|
||||
int cur = cursorAddress;
|
||||
int curRow = cur / cols;
|
||||
int curCol = cur % cols;
|
||||
|
||||
if (tabStops != null && tabStops.length > 0) {
|
||||
if (forward) {
|
||||
for (int stop : tabStops) {
|
||||
if (stop > curCol && stop < cols) {
|
||||
setCursorPosition(curRow, stop);
|
||||
return;
|
||||
}
|
||||
}
|
||||
int nextRow = (curRow + 1) % rows;
|
||||
setCursorPosition(nextRow, tabStops[0]);
|
||||
return;
|
||||
} else {
|
||||
for (int i = tabStops.length - 1; i >= 0; i--) {
|
||||
int stop = tabStops[i];
|
||||
if (stop < curCol && stop >= 0) {
|
||||
setCursorPosition(curRow, stop);
|
||||
return;
|
||||
}
|
||||
}
|
||||
int prevRow = (curRow - 1 + rows) % rows;
|
||||
setCursorPosition(prevRow, tabStops[tabStops.length - 1]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard Word Tab: Jump to next / prev word boundary
|
||||
if (forward) {
|
||||
int addr = cur;
|
||||
int count = 0;
|
||||
while (count < size && getChar(addr / cols, addr % cols) != ' ' && !getCell(addr).isFieldAttribute()) {
|
||||
addr = incrementAddress(addr);
|
||||
count++;
|
||||
}
|
||||
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
|
||||
addr = incrementAddress(addr);
|
||||
count++;
|
||||
}
|
||||
setCursorAddress(addr);
|
||||
} else {
|
||||
int addr = decrementAddress(cur);
|
||||
int count = 0;
|
||||
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
|
||||
addr = decrementAddress(addr);
|
||||
count++;
|
||||
}
|
||||
while (count < size && getChar(decrementAddress(addr) / cols, decrementAddress(addr) % cols) != ' '
|
||||
&& !getCell(decrementAddress(addr)).isFieldAttribute()) {
|
||||
addr = decrementAddress(addr);
|
||||
count++;
|
||||
}
|
||||
setCursorAddress(addr);
|
||||
}
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public synchronized void processDeleteWord() {
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return;
|
||||
int cur = cursorAddress;
|
||||
if (formatted) {
|
||||
byte fa = getFieldAttributeAt(cur);
|
||||
if (faIsProtected(fa & 0xFF)) return;
|
||||
}
|
||||
|
||||
int endWord = cur;
|
||||
int count = 0;
|
||||
while (count < size && !getCell(endWord).isFieldAttribute()) {
|
||||
char ch = getChar(endWord / cols, endWord % cols);
|
||||
endWord = incrementAddress(endWord);
|
||||
count++;
|
||||
if (ch == ' ') break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
deleteChar(cur);
|
||||
}
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
// ========== DBCS Shift-Out / Shift-In Display Transformation ==========
|
||||
|
||||
public synchronized void processSOSI() {
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return;
|
||||
boolean insideDBCS = false;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = buffer[i];
|
||||
if (ea.isFieldAttribute()) {
|
||||
insideDBCS = false;
|
||||
continue;
|
||||
}
|
||||
int ec = ea.ec & 0xFF;
|
||||
if (ec == 0x0E) { // Shift-Out
|
||||
insideDBCS = true;
|
||||
ea.db = ExtendedAttribute.DB_SO;
|
||||
ea.ucs4 = ' ';
|
||||
} else if (ec == 0x0F) { // Shift-In
|
||||
insideDBCS = false;
|
||||
ea.db = ExtendedAttribute.DB_SI;
|
||||
ea.ucs4 = ' ';
|
||||
} else if (insideDBCS) {
|
||||
int nextIdx = (i + 1) % size;
|
||||
ExtendedAttribute nextEa = buffer[nextIdx];
|
||||
if (!nextEa.isFieldAttribute() && (nextEa.ec & 0xFF) != 0x0F) {
|
||||
int b1 = ec;
|
||||
int b2 = nextEa.ec & 0xFF;
|
||||
ea.cs = ExtendedAttribute.CS_DBCS;
|
||||
ea.db = ExtendedAttribute.DB_LEFT;
|
||||
nextEa.cs = ExtendedAttribute.CS_DBCS;
|
||||
nextEa.db = ExtendedAttribute.DB_RIGHT;
|
||||
|
||||
if (translator != null && translator.isDBCS() && translator.getCodePage() != null) {
|
||||
char uni = translator.getCodePage().dbcsToUnicode(b1, b2);
|
||||
ea.ucs4 = uni;
|
||||
nextEa.ucs4 = uni;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
ea.db = ExtendedAttribute.DB_NONE;
|
||||
if (ea.cs == ExtendedAttribute.CS_DBCS) {
|
||||
ea.cs = ExtendedAttribute.CS_BASE;
|
||||
}
|
||||
}
|
||||
}
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,44 +40,71 @@ public class TelnetConnection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the host. Blocks until connection is established or fails.
|
||||
* Connect to the host (optionally through a proxy). Blocks until connection is established or fails.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
ConnectionConfig.ProxyType proxyType = config.getProxyType();
|
||||
boolean hasProxy = proxyType != null && proxyType != ConnectionConfig.ProxyType.NONE &&
|
||||
config.getProxyHost() != null && !config.getProxyHost().trim().isEmpty();
|
||||
|
||||
String connectHost = hasProxy ? config.getProxyHost().trim() : config.getHost();
|
||||
int connectPort = hasProxy ? (config.getProxyPort() > 0 ? config.getProxyPort() : (proxyType == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080)) : config.getPort();
|
||||
|
||||
log.info("Connecting TCP socket to " + connectHost + ":" + connectPort +
|
||||
(hasProxy ? " (via " + proxyType + " proxy for " + config.getHost() + ":" + config.getPort() + ")" : "") +
|
||||
(config.isUseTls() ? " with TLS" : ""));
|
||||
|
||||
Socket rawSocket = new Socket();
|
||||
rawSocket.setKeepAlive(config.isSoKeepAlive());
|
||||
rawSocket.setOOBInline(true);
|
||||
rawSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
rawSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
|
||||
|
||||
// Perform proxy handshake if configured
|
||||
if (hasProxy) {
|
||||
switch (proxyType) {
|
||||
case HTTP:
|
||||
establishHttpProxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword());
|
||||
break;
|
||||
case SOCKS4:
|
||||
establishSocks4Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername());
|
||||
break;
|
||||
case SOCKS5:
|
||||
establishSocks5Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.isUseTls()) {
|
||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
||||
log.info("Performing TLS handshake with " + config.getHost() + ":" + config.getPort() +
|
||||
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
||||
try {
|
||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(
|
||||
rawSocket, config.getHost(), config.getPort(), true);
|
||||
sslSocket.setKeepAlive(config.isSoKeepAlive());
|
||||
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
sslSession = sslSocket.getSession();
|
||||
log.info("TLS session active: protocol=" + sslSession.getProtocol() +
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("TLS setup failure: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||
socket = new Socket();
|
||||
socket.setKeepAlive(config.isSoKeepAlive());
|
||||
socket.setOOBInline(true);
|
||||
socket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
socket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
socket = rawSocket;
|
||||
}
|
||||
|
||||
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
|
||||
@@ -91,6 +118,235 @@ public class TelnetConnection {
|
||||
readerThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically elevate active socket to TLS in-band (STARTTLS / Option 46).
|
||||
*/
|
||||
public synchronized void upgradeToTls() throws IOException {
|
||||
if (socket == null || !socket.isConnected() || socket.isClosed()) {
|
||||
throw new IOException("Cannot upgrade disconnected socket to TLS");
|
||||
}
|
||||
log.info("Elevating active connection to TLS via STARTTLS");
|
||||
try {
|
||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(
|
||||
socket, config.getHost(), config.getPort(), true);
|
||||
sslSocket.setKeepAlive(config.isSoKeepAlive());
|
||||
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
sslSocket.startHandshake();
|
||||
this.socket = sslSocket;
|
||||
this.sslSession = sslSocket.getSession();
|
||||
this.inputStream = new BufferedInputStream(sslSocket.getInputStream(), READ_BUFFER_SIZE);
|
||||
this.outputStream = new BufferedOutputStream(sslSocket.getOutputStream());
|
||||
log.info("STARTTLS session active: protocol=" + sslSession.getProtocol() +
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("STARTTLS setup failure: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
StringBuilder req = new StringBuilder();
|
||||
req.append("CONNECT ").append(targetHost).append(":").append(targetPort).append(" HTTP/1.1\r\n");
|
||||
req.append("Host: ").append(targetHost).append(":").append(targetPort).append("\r\n");
|
||||
if (user != null && !user.isEmpty()) {
|
||||
String auth = user + ":" + (pass != null ? pass : "");
|
||||
String encoded = java.util.Base64.getEncoder().encodeToString(auth.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
req.append("Proxy-Authorization: Basic ").append(encoded).append("\r\n");
|
||||
}
|
||||
req.append("Proxy-Connection: Keep-Alive\r\n\r\n");
|
||||
out.write(req.toString().getBytes(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
out.flush();
|
||||
|
||||
// Read HTTP status line
|
||||
ByteArrayOutputStream lineBuf = new ByteArrayOutputStream();
|
||||
int b;
|
||||
while ((b = in.read()) != -1) {
|
||||
if (b == '\n') break;
|
||||
if (b != '\r') lineBuf.write(b);
|
||||
}
|
||||
String statusLine = new String(lineBuf.toByteArray(), java.nio.charset.StandardCharsets.US_ASCII);
|
||||
if (!statusLine.contains(" 200")) {
|
||||
throw new IOException("HTTP proxy connection failed: " + statusLine);
|
||||
}
|
||||
|
||||
// Consume remaining response headers until empty line
|
||||
while (true) {
|
||||
lineBuf.reset();
|
||||
while ((b = in.read()) != -1) {
|
||||
if (b == '\n') break;
|
||||
if (b != '\r') lineBuf.write(b);
|
||||
}
|
||||
if (lineBuf.size() == 0) break; // empty line terminates headers
|
||||
}
|
||||
log.info("HTTP proxy tunnel established to " + targetHost + ":" + targetPort);
|
||||
}
|
||||
|
||||
private void establishSocks4Proxy(Socket s, String targetHost, int targetPort, String user) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
byte[] ip = new byte[4];
|
||||
boolean isSocks4a = false;
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(targetHost);
|
||||
if (addr instanceof Inet4Address) {
|
||||
ip = addr.getAddress();
|
||||
} else {
|
||||
isSocks4a = true;
|
||||
ip = new byte[] { 0, 0, 0, 1 };
|
||||
}
|
||||
} catch (Exception e) {
|
||||
isSocks4a = true;
|
||||
ip = new byte[] { 0, 0, 0, 1 };
|
||||
}
|
||||
|
||||
ByteArrayOutputStream req = new ByteArrayOutputStream();
|
||||
req.write(0x04); // SOCKS version 4
|
||||
req.write(0x01); // CONNECT command
|
||||
req.write((targetPort >> 8) & 0xFF);
|
||||
req.write(targetPort & 0xFF);
|
||||
req.write(ip);
|
||||
if (user != null && !user.isEmpty()) {
|
||||
req.write(user.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
req.write(0x00); // Null terminator for userid
|
||||
if (isSocks4a) {
|
||||
req.write(targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
req.write(0x00); // Null terminator for domain name
|
||||
}
|
||||
|
||||
out.write(req.toByteArray());
|
||||
out.flush();
|
||||
|
||||
byte[] resp = new byte[8];
|
||||
int read = 0;
|
||||
while (read < 8) {
|
||||
int n = in.read(resp, read, 8 - read);
|
||||
if (n < 0) throw new IOException("Unexpected EOF reading SOCKS4 response");
|
||||
read += n;
|
||||
}
|
||||
|
||||
int status = resp[1] & 0xFF;
|
||||
if (status != 0x5A) {
|
||||
throw new IOException("SOCKS4 proxy request rejected, status=0x" + Integer.toHexString(status));
|
||||
}
|
||||
log.info("SOCKS4 proxy tunnel established to " + targetHost + ":" + targetPort);
|
||||
}
|
||||
|
||||
private void establishSocks5Proxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
boolean hasAuth = user != null && !user.isEmpty();
|
||||
if (hasAuth) {
|
||||
out.write(new byte[] { 0x05, 0x02, 0x00, 0x02 }); // SOCKS5, 2 methods: NO_AUTH(0x00), USER_PASS(0x02)
|
||||
} else {
|
||||
out.write(new byte[] { 0x05, 0x01, 0x00 }); // SOCKS5, 1 method: NO_AUTH(0x00)
|
||||
}
|
||||
out.flush();
|
||||
|
||||
byte[] methodResp = new byte[2];
|
||||
readFully(in, methodResp);
|
||||
if ((methodResp[0] & 0xFF) != 0x05) {
|
||||
throw new IOException("Invalid SOCKS5 version response: " + (methodResp[0] & 0xFF));
|
||||
}
|
||||
|
||||
int authMethod = methodResp[1] & 0xFF;
|
||||
if (authMethod == 0x02) {
|
||||
// RFC 1929 Username/Password Authentication
|
||||
byte[] uBytes = user.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
byte[] pBytes = (pass != null ? pass : "").getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream authReq = new ByteArrayOutputStream();
|
||||
authReq.write(0x01); // Auth subnegotiation version
|
||||
authReq.write(uBytes.length);
|
||||
authReq.write(uBytes);
|
||||
authReq.write(pBytes.length);
|
||||
authReq.write(pBytes);
|
||||
out.write(authReq.toByteArray());
|
||||
out.flush();
|
||||
|
||||
byte[] authResp = new byte[2];
|
||||
readFully(in, authResp);
|
||||
if (authResp[1] != 0x00) {
|
||||
throw new IOException("SOCKS5 username/password authentication failed");
|
||||
}
|
||||
} else if (authMethod != 0x00) {
|
||||
throw new IOException("SOCKS5 proxy authentication method rejected: 0x" + Integer.toHexString(authMethod));
|
||||
}
|
||||
|
||||
// Send CONNECT request
|
||||
ByteArrayOutputStream connReq = new ByteArrayOutputStream();
|
||||
connReq.write(0x05); // SOCKS5
|
||||
connReq.write(0x01); // CONNECT
|
||||
connReq.write(0x00); // Reserved
|
||||
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(targetHost);
|
||||
if (addr instanceof Inet4Address) {
|
||||
connReq.write(0x01); // ATYP IPv4
|
||||
connReq.write(addr.getAddress());
|
||||
} else if (addr instanceof Inet6Address) {
|
||||
connReq.write(0x04); // ATYP IPv6
|
||||
connReq.write(addr.getAddress());
|
||||
} else {
|
||||
byte[] dBytes = targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
connReq.write(0x03); // ATYP Domain
|
||||
connReq.write(dBytes.length);
|
||||
connReq.write(dBytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
byte[] dBytes = targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
connReq.write(0x03); // ATYP Domain
|
||||
connReq.write(dBytes.length);
|
||||
connReq.write(dBytes);
|
||||
}
|
||||
|
||||
connReq.write((targetPort >> 8) & 0xFF);
|
||||
connReq.write(targetPort & 0xFF);
|
||||
out.write(connReq.toByteArray());
|
||||
out.flush();
|
||||
|
||||
byte[] connResp = new byte[4];
|
||||
readFully(in, connResp);
|
||||
int rep = connResp[1] & 0xFF;
|
||||
if (rep != 0x00) {
|
||||
throw new IOException("SOCKS5 connect command failed, rep=0x" + Integer.toHexString(rep));
|
||||
}
|
||||
|
||||
int atyp = connResp[3] & 0xFF;
|
||||
if (atyp == 0x01) {
|
||||
byte[] bnd = new byte[4 + 2]; // IPv4 + Port
|
||||
readFully(in, bnd);
|
||||
} else if (atyp == 0x03) {
|
||||
int len = in.read();
|
||||
if (len < 0) throw new IOException("Unexpected EOF in SOCKS5 domain response");
|
||||
byte[] bnd = new byte[len + 2]; // Domain + Port
|
||||
readFully(in, bnd);
|
||||
} else if (atyp == 0x04) {
|
||||
byte[] bnd = new byte[16 + 2]; // IPv6 + Port
|
||||
readFully(in, bnd);
|
||||
}
|
||||
log.info("SOCKS5 proxy tunnel established to " + targetHost + ":" + targetPort);
|
||||
}
|
||||
|
||||
private static void readFully(InputStream in, byte[] buf) throws IOException {
|
||||
int read = 0;
|
||||
while (read < buf.length) {
|
||||
int n = in.read(buf, read, buf.length - read);
|
||||
if (n < 0) throw new IOException("Unexpected EOF reading proxy response");
|
||||
read += n;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send raw bytes to the host.
|
||||
*/
|
||||
|
||||
@@ -107,19 +107,20 @@ public class TelnetFSM {
|
||||
// Listeners
|
||||
private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<SCSInboundListener> scsListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Connected LU info
|
||||
private String connectedLu;
|
||||
private String connectedType;
|
||||
|
||||
enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
|
||||
public enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
|
||||
|
||||
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
|
||||
this.config = config;
|
||||
this.screenBuffer = screenBuffer;
|
||||
this.dsProcessor = dsProcessor;
|
||||
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||
this.nvtProcessor.setOutputSender(this::sendBytes);
|
||||
this.nvtProcessor.setOutputSender(this::sendNvtData);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
|
||||
@@ -131,10 +132,25 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
|
||||
public void removeConnectionListener(ConnectionListener l) { connectionListeners.remove(l); }
|
||||
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.add(l);
|
||||
nvtProcessor.addScreenUpdateListener(l);
|
||||
}
|
||||
public void removeScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.remove(l);
|
||||
nvtProcessor.removeScreenUpdateListener(l);
|
||||
}
|
||||
|
||||
public void addSCSInboundListener(SCSInboundListener l) {
|
||||
if (l != null && !scsListeners.contains(l)) {
|
||||
scsListeners.add(l);
|
||||
}
|
||||
}
|
||||
public void removeSCSInboundListener(SCSInboundListener l) {
|
||||
scsListeners.remove(l);
|
||||
}
|
||||
|
||||
public ConnectionState getConnectionState() { return connectionState; }
|
||||
public boolean[] getMyOpts() { return myOpts; }
|
||||
@@ -189,9 +205,11 @@ public class TelnetFSM {
|
||||
if (connectionState == ConnectionState.TELNET_PENDING) {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
if (connectionState.isNvt()) {
|
||||
boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
|
||||
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
|
||||
if (isPlainNvt) {
|
||||
nvtProcessor.processNVTData(buf, start, i - start);
|
||||
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
||||
ibuf.write(buf, start, i - start);
|
||||
}
|
||||
}
|
||||
@@ -260,13 +278,15 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
|
||||
if (connectionState.isNvt()) {
|
||||
boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
|
||||
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
|
||||
if (isPlainNvt) {
|
||||
nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
// Accumulate data for 3270, TN3270E (including CONNECTED_E_NVT, SSCP-LU and unbound states, and pending)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
||||
ibuf.write(c);
|
||||
}
|
||||
}
|
||||
@@ -336,6 +356,13 @@ public class TelnetFSM {
|
||||
sendCommand(DO, opt);
|
||||
break;
|
||||
|
||||
case TELOPT_STARTTLS:
|
||||
if (config.isStartTlsEnabled() && !hisOpts[opt]) {
|
||||
hisOpts[opt] = true;
|
||||
sendCommand(DO, opt);
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_TN3270E:
|
||||
if (!config.isTn3270eEnabled()) {
|
||||
sendCommand(DONT, opt);
|
||||
@@ -384,6 +411,13 @@ public class TelnetFSM {
|
||||
sendCommand(WILL, opt);
|
||||
break;
|
||||
|
||||
case TELOPT_STARTTLS:
|
||||
if (config.isStartTlsEnabled() && !myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
sendCommand(WILL, opt);
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_TTYPE:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
@@ -489,12 +523,37 @@ public class TelnetFSM {
|
||||
case TELOPT_NEW_ENVIRON:
|
||||
handleNewEnvironSB(data);
|
||||
break;
|
||||
case TELOPT_STARTTLS:
|
||||
handleStartTlsSB(data);
|
||||
break;
|
||||
default:
|
||||
log.info("Ignoring SB for option " + opt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== STARTTLS sub-negotiation ==========
|
||||
|
||||
private void handleStartTlsSB(byte[] data) {
|
||||
if (data.length >= 2 && (data[1] & 0xFF) == TLS_FOLLOWS) {
|
||||
log.info("RCVD SB STARTTLS FOLLOWS (1) - Initiating TLS elevation");
|
||||
processStartTls();
|
||||
}
|
||||
}
|
||||
|
||||
public void processStartTls() {
|
||||
log.info("Elevating active connection to TLS via STARTTLS (Option 46)");
|
||||
try {
|
||||
if (connection != null) {
|
||||
connection.upgradeToTls();
|
||||
statusDisplay(STATUS_SECURITY, "TLS socket elevated via STARTTLS");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.log(Level.SEVERE, "Failed to elevate socket to TLS via STARTTLS", e);
|
||||
onError("STARTTLS elevation failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TTYPE sub-negotiation ==========
|
||||
|
||||
private void handleTTypeSB(byte[] data) {
|
||||
@@ -522,15 +581,117 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== NEW_ENVIRON sub-negotiation ==========
|
||||
// ========== NEW_ENVIRON sub-negotiation (RFC 1572 / RFC 2877) ==========
|
||||
|
||||
private void handleNewEnvironSB(byte[] data) {
|
||||
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
|
||||
log.info("RCVD SB NEW-ENVIRON SEND - Responding with empty IS");
|
||||
byte[] response = { (byte) IAC, (byte) SB, (byte) TELOPT_NEW_ENVIRON,
|
||||
(byte) TELQUAL_IS, (byte) IAC, (byte) SE };
|
||||
sendBytes(response);
|
||||
log.info("SENT SB NEW-ENVIRON IS SE");
|
||||
if (data.length < 2) return;
|
||||
int qual = data[1] & 0xFF;
|
||||
if (qual == TELQUAL_SEND) {
|
||||
log.info("RCVD SB NEW-ENVIRON SEND (" + (data.length - 2) + " bytes)");
|
||||
if (data.length == 2) {
|
||||
// Empty SEND: send all configured variables
|
||||
sendNewEnvironmentVariables(config.getEnvironmentVariables(), config.getUserVariables());
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse requested variable names
|
||||
java.util.Map<String, String> respVars = new java.util.LinkedHashMap<>();
|
||||
java.util.Map<String, String> respUserVars = new java.util.LinkedHashMap<>();
|
||||
|
||||
int idx = 2;
|
||||
while (idx < data.length) {
|
||||
int objType = data[idx++] & 0xFF;
|
||||
ByteArrayOutputStream nameBuf = new ByteArrayOutputStream();
|
||||
boolean escaped = false;
|
||||
while (idx < data.length) {
|
||||
int b = data[idx] & 0xFF;
|
||||
if (!escaped && (b == TELOBJ_VAR || b == TELOBJ_USERVAR)) {
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
if (!escaped && b == TELOBJ_ESC) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
nameBuf.write(b);
|
||||
escaped = false;
|
||||
}
|
||||
|
||||
String varName = new String(nameBuf.toByteArray(), java.nio.charset.StandardCharsets.US_ASCII);
|
||||
if (objType == TELOBJ_VAR) {
|
||||
if (varName.isEmpty()) {
|
||||
respVars.putAll(config.getEnvironmentVariables());
|
||||
} else if (config.getEnvironmentVariables().containsKey(varName)) {
|
||||
respVars.put(varName, config.getEnvironmentVariables().get(varName));
|
||||
}
|
||||
} else if (objType == TELOBJ_USERVAR) {
|
||||
if (varName.isEmpty()) {
|
||||
respUserVars.putAll(config.getUserVariables());
|
||||
} else if (config.getUserVariables().containsKey(varName)) {
|
||||
respUserVars.put(varName, config.getUserVariables().get(varName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendNewEnvironmentVariables(respVars, respUserVars);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendNewEnvironmentVariables(java.util.Properties props) {
|
||||
java.util.Map<String, String> vars = new java.util.LinkedHashMap<>();
|
||||
java.util.Map<String, String> uVars = new java.util.LinkedHashMap<>();
|
||||
if (props != null) {
|
||||
for (String k : props.stringPropertyNames()) {
|
||||
if (k.startsWith("USERVAR_") || k.startsWith("USER_")) {
|
||||
uVars.put(k, props.getProperty(k));
|
||||
} else {
|
||||
vars.put(k, props.getProperty(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
sendNewEnvironmentVariables(vars, uVars);
|
||||
}
|
||||
|
||||
public void sendNewEnvironmentVariables(java.util.Map<String, String> vars, java.util.Map<String, String> userVars) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(IAC);
|
||||
out.write(SB);
|
||||
out.write(TELOPT_NEW_ENVIRON);
|
||||
out.write(TELQUAL_IS);
|
||||
|
||||
if (vars != null) {
|
||||
for (java.util.Map.Entry<String, String> e : vars.entrySet()) {
|
||||
out.write(TELOBJ_VAR);
|
||||
writeEscapedEnvironString(out, e.getKey());
|
||||
out.write(TELOBJ_VALUE);
|
||||
writeEscapedEnvironString(out, e.getValue());
|
||||
}
|
||||
}
|
||||
if (userVars != null) {
|
||||
for (java.util.Map.Entry<String, String> e : userVars.entrySet()) {
|
||||
out.write(TELOBJ_USERVAR);
|
||||
writeEscapedEnvironString(out, e.getKey());
|
||||
out.write(TELOBJ_VALUE);
|
||||
writeEscapedEnvironString(out, e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.info("SENT SB NEW-ENVIRON IS (" + ((vars != null ? vars.size() : 0) + (userVars != null ? userVars.size() : 0)) + " vars) SE");
|
||||
}
|
||||
|
||||
private void writeEscapedEnvironString(ByteArrayOutputStream out, String s) {
|
||||
if (s == null) return;
|
||||
for (byte b : s.getBytes(java.nio.charset.StandardCharsets.US_ASCII)) {
|
||||
int ub = b & 0xFF;
|
||||
if (ub == TELOBJ_VAR || ub == TELOBJ_VALUE || ub == TELOBJ_ESC || ub == TELOBJ_USERVAR) {
|
||||
out.write(TELOBJ_ESC);
|
||||
} else if (ub == IAC) {
|
||||
out.write(IAC);
|
||||
}
|
||||
out.write(ub);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,8 +977,10 @@ public class TelnetFSM {
|
||||
ibuf.reset();
|
||||
if (data.length == 0) return;
|
||||
|
||||
if (connectionState == ConnectionState.TELNET_PENDING && !tn3270eNegotiated) {
|
||||
log.info("Received EOR during TELNET_PENDING - transitioning to plain TN3270 mode");
|
||||
if ((connectionState == ConnectionState.TELNET_PENDING ||
|
||||
connectionState == ConnectionState.CONNECTED_NVT ||
|
||||
connectionState == ConnectionState.CONNECTED_NVT_CHAR) && !tn3270eNegotiated) {
|
||||
log.info("Received EOR during NVT/pending - transitioning to plain TN3270 mode");
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
}
|
||||
|
||||
@@ -831,6 +994,10 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
public void processTn3270eHeader(byte[] data) {
|
||||
processTN3270ERecord(data);
|
||||
}
|
||||
|
||||
private void processTN3270ERecord(byte[] data) {
|
||||
if (data.length < EH_SIZE) {
|
||||
log.warning("TN3270E record too short: " + data.length);
|
||||
@@ -850,11 +1017,10 @@ public class TelnetFSM {
|
||||
switch (dataType) {
|
||||
case DT_3270_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
// Transition to 3270 mode
|
||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
|
||||
connectionState == ConnectionState.CONNECTED_SSCP) {
|
||||
// Clear screen on transition to 3270 mode from unbound/SSCP
|
||||
// This ensures old SSCP-LU data or stale content doesn't persist
|
||||
// Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP)
|
||||
if (connectionState != ConnectionState.CONNECTED_TN3270E) {
|
||||
// Clear screen on transition to 3270 mode from unbound/SSCP/NVT
|
||||
// This ensures old SSCP-LU or NVT data doesn't persist
|
||||
screenBuffer.erase(false);
|
||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
@@ -879,9 +1045,30 @@ public class TelnetFSM {
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_SCS_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
try {
|
||||
processSCSInbound(data, EH_SIZE, data.length - EH_SIZE);
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error processing SCS record", e);
|
||||
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
|
||||
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_SSCP_LU_DATA:
|
||||
if (connectionState != ConnectionState.CONNECTED_SSCP) {
|
||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND) {
|
||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
|
||||
connectionState == ConnectionState.CONNECTED_E_NVT) {
|
||||
// Clear screen on first SSCP-LU transition to remove stale data
|
||||
screenBuffer.clear();
|
||||
}
|
||||
@@ -919,8 +1106,13 @@ public class TelnetFSM {
|
||||
|
||||
case DT_NVT_DATA:
|
||||
// NVT data in TN3270E mode
|
||||
changeState(ConnectionState.CONNECTED_E_NVT);
|
||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||
if (connectionState != ConnectionState.CONNECTED_E_NVT) {
|
||||
changeState(ConnectionState.CONNECTED_E_NVT);
|
||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||
}
|
||||
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
if (data.length > EH_SIZE) {
|
||||
try {
|
||||
processNVTData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
@@ -938,6 +1130,7 @@ public class TelnetFSM {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
}
|
||||
notifyScreenUpdate();
|
||||
break;
|
||||
|
||||
case DT_REQUEST:
|
||||
@@ -957,6 +1150,16 @@ public class TelnetFSM {
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_BID:
|
||||
process_BID(responseFlag, seqNumber);
|
||||
break;
|
||||
|
||||
case DT_PRINT_EOJ:
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_RESPONSE:
|
||||
lastRcvSeq = seqNumber;
|
||||
log.fine("Received response, seq=" + seqNumber);
|
||||
@@ -966,8 +1169,8 @@ public class TelnetFSM {
|
||||
// Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.)
|
||||
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
|
||||
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
|
||||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 || dataType == 0x05 ||
|
||||
dataType == 0x0D || dataType == 0x01 || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
|
||||
dataType == 0x0D || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
|
||||
") in TN3270E mode — automatically switching to plain TN3270 mode");
|
||||
tn3270eNegotiated = false;
|
||||
@@ -981,28 +1184,56 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendTN3270EPositiveResponse(int seqNumber) {
|
||||
public void processSCSInbound(byte[] data) {
|
||||
if (data == null) return;
|
||||
processSCSInbound(data, 0, data.length);
|
||||
}
|
||||
|
||||
public void processSCSInbound(byte[] data, int offset, int length) {
|
||||
log.fine("Processing SCS inbound data (" + length + " bytes)");
|
||||
for (SCSInboundListener l : scsListeners) {
|
||||
try {
|
||||
l.onSCSDataReceived(data, offset, length);
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error in SCS inbound listener", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send 5-byte/6-byte TN3270E response packet (DT_RESPONSE = 0x02).
|
||||
*/
|
||||
public void sendTn3270eResponse(byte responseFlag, byte responseData, int seq) {
|
||||
byte[] resp = new byte[EH_SIZE + 1];
|
||||
resp[0] = (byte) DT_RESPONSE;
|
||||
resp[1] = 0;
|
||||
resp[2] = (byte) RSF_POSITIVE_RESPONSE;
|
||||
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seqNumber & 0xFF);
|
||||
resp[5] = (byte) POS_DEVICE_END;
|
||||
resp[2] = responseFlag;
|
||||
resp[3] = (byte) ((seq >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seq & 0xFF);
|
||||
resp[5] = responseData;
|
||||
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
|
||||
byte[] resp = new byte[EH_SIZE + 1];
|
||||
resp[0] = (byte) DT_RESPONSE;
|
||||
resp[1] = 0;
|
||||
resp[2] = (byte) RSF_NEGATIVE_RESPONSE;
|
||||
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seqNumber & 0xFF);
|
||||
resp[5] = (byte) (negCode & 0xFF);
|
||||
/**
|
||||
* HoD 5-byte send_response compatible signature (com.ibm.eNetwork.ECL.tn3270.Telnet3270E).
|
||||
*/
|
||||
public void send_response(short s, short s2, int n) {
|
||||
byte[] byArray = new byte[5];
|
||||
byArray[0] = (byte) DT_RESPONSE;
|
||||
byArray[1] = (byte) s;
|
||||
byArray[2] = (byte) s2;
|
||||
byArray[3] = (byte) ((n >> 8) & 0xFF);
|
||||
byArray[4] = (byte) (n & 0xFF);
|
||||
sendRecord(byArray);
|
||||
}
|
||||
|
||||
sendRecord(resp);
|
||||
public void sendTN3270EPositiveResponse(int seqNumber) {
|
||||
sendTn3270eResponse((byte) RSF_POSITIVE_RESPONSE, (byte) POS_DEVICE_END, seqNumber);
|
||||
}
|
||||
|
||||
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
|
||||
sendTn3270eResponse((byte) RSF_NEGATIVE_RESPONSE, (byte) (negCode & 0xFF), seqNumber);
|
||||
}
|
||||
|
||||
public void sendTN3270ESnaSenseResponse(int seqNumber, int sense1, int sense2) {
|
||||
@@ -1021,7 +1252,11 @@ public class TelnetFSM {
|
||||
// ========== Check if we should transition to 3270 mode ==========
|
||||
|
||||
private void checkIn3270() {
|
||||
if (connectionState != ConnectionState.TELNET_PENDING) return;
|
||||
if (connectionState != ConnectionState.TELNET_PENDING &&
|
||||
connectionState != ConnectionState.CONNECTED_NVT &&
|
||||
connectionState != ConnectionState.CONNECTED_NVT_CHAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For TN3270E, we wait for TN3270E negotiation to complete
|
||||
if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
|
||||
@@ -1032,7 +1267,11 @@ public class TelnetFSM {
|
||||
if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] &&
|
||||
myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) {
|
||||
log.info("Transitioning to plain TN3270 mode");
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
if (connectionState != ConnectionState.CONNECTED_3270) {
|
||||
screenBuffer.erase(false);
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1130,6 +1369,32 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send NVT data record (with TN3270E header and EOR framing if in TN3270E mode, or raw bytes).
|
||||
*/
|
||||
public void sendNvtData(byte[] data) {
|
||||
if (data == null || data.length == 0) return;
|
||||
|
||||
if (tn3270eNegotiated && (connectionState == ConnectionState.CONNECTED_E_NVT || tn3270eSubmode == TN3270ESubmode.E_NVT)) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
|
||||
out.write(DT_NVT_DATA);
|
||||
out.write(0);
|
||||
out.write(0);
|
||||
out.write((eXmitSeq >> 8) & 0xFF);
|
||||
out.write(eXmitSeq & 0xFF);
|
||||
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
|
||||
for (byte b : data) {
|
||||
out.write(b & 0xFF);
|
||||
}
|
||||
sendRecord(out.toByteArray());
|
||||
} else {
|
||||
sendBytes(data);
|
||||
if (config != null && config.isNvtLocalEcho()) {
|
||||
nvtProcessor.processNVTData(data, 0, data.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBytes(byte[] data) {
|
||||
try {
|
||||
connection.sendRaw(data);
|
||||
@@ -1170,6 +1435,7 @@ public class TelnetFSM {
|
||||
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
|
||||
public String getConnectedLu() { return connectedLu; }
|
||||
public String getConnectedType() { return connectedType; }
|
||||
public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; }
|
||||
|
||||
// ========== SNA BIND, UNBIND, BID, DFC Handlers (Phase 2) ==========
|
||||
|
||||
@@ -1434,6 +1700,10 @@ public class TelnetFSM {
|
||||
return tn3270eBound;
|
||||
}
|
||||
|
||||
public void processSysReq() {
|
||||
handleSysReq();
|
||||
}
|
||||
|
||||
public void handleSysReq() {
|
||||
if (tn3270eNegotiated) {
|
||||
byte[] ao = new byte[] { (byte) IAC, (byte) AO };
|
||||
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
import haus.nightmare.lib3270j.printer.PrintSCS3270;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Comprehensive test suite verifying all Phase 2 functional updates:
|
||||
* 3270 Data Stream parsing, Graphic Escape & APL mapping, Partitioning,
|
||||
* Structured Fields (0x0F, 0x20, 0x24, 0x40), and Query Reply builders.
|
||||
*/
|
||||
public class DataStreamProcessorPhase2FullTest {
|
||||
|
||||
private EbcdicTranslator translator;
|
||||
private ScreenBuffer screen;
|
||||
private DataStreamProcessor processor;
|
||||
private ByteArrayOutputStream output;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
processor = new DataStreamProcessor(screen, translator);
|
||||
output = new ByteArrayOutputStream();
|
||||
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGraphicEscapeOrderWithAPLTranslation() {
|
||||
// Write record: CMD_EW (0x05), WCC (0xC3), SBA to 0 (0x11, 0x40, 0x40), ORDER_GE (0x08), 0xA2 (s -> ─)
|
||||
// followed by ORDER_GE, 0x85 (e -> │), ORDER_GE, 0xCB (Cross -> ┼), ORDER_GE, 0xBA (Omega -> Ω)
|
||||
byte[] record = new byte[] {
|
||||
(byte) CMD_EW, (byte) 0xC3,
|
||||
(byte) ORDER_SBA, 0x40, 0x40,
|
||||
(byte) ORDER_GE, (byte) 0xA2, // Horizontal Line
|
||||
(byte) ORDER_GE, (byte) 0x85, // Vertical Line
|
||||
(byte) ORDER_GE, (byte) 0xCB, // Cross
|
||||
(byte) ORDER_GE, (byte) 0xBA // Greek Omega
|
||||
};
|
||||
|
||||
processor.processRecord(record);
|
||||
|
||||
ExtendedAttribute cell0 = screen.getCell(0);
|
||||
assertEquals(CS_GE, cell0.cs);
|
||||
assertEquals((byte) 0xA2, cell0.ec);
|
||||
assertEquals('─', cell0.ucs4);
|
||||
|
||||
ExtendedAttribute cell1 = screen.getCell(1);
|
||||
assertEquals(CS_GE, cell1.cs);
|
||||
assertEquals((byte) 0x85, cell1.ec);
|
||||
assertEquals('│', cell1.ucs4);
|
||||
|
||||
ExtendedAttribute cell2 = screen.getCell(2);
|
||||
assertEquals(CS_GE, cell2.cs);
|
||||
assertEquals((byte) 0xCB, cell2.ec);
|
||||
assertEquals('┼', cell2.ucs4);
|
||||
|
||||
ExtendedAttribute cell3 = screen.getCell(3);
|
||||
assertEquals(CS_GE, cell3.cs);
|
||||
assertEquals((byte) 0xBA, cell3.ec);
|
||||
assertEquals('Ω', cell3.ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatToAddressWithGraphicEscape() {
|
||||
// Repeat to address 5 with ORDER_GE prefix + 0xA2 ('─')
|
||||
byte[] record = new byte[] {
|
||||
(byte) CMD_EW, (byte) 0xC3,
|
||||
(byte) ORDER_SBA, 0x40, 0x40, // Addr 0
|
||||
(byte) ORDER_RA, 0x40, (byte) 0x45, // Addr 5
|
||||
(byte) ORDER_GE, (byte) 0xA2
|
||||
};
|
||||
|
||||
processor.processRecord(record);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ExtendedAttribute cell = screen.getCell(i);
|
||||
assertEquals(CS_GE, cell.cs, "Cell " + i + " should have CS_GE");
|
||||
assertEquals('─', cell.ucs4, "Cell " + i + " should have '─'");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutbound3270DSNonZeroPartition() {
|
||||
// First create partition 2 (32 rows x 80 cols)
|
||||
byte[] createPart = new byte[] {
|
||||
(byte) CMD_WSF,
|
||||
0x00, 0x08,
|
||||
(byte) SF_CREATE_PART, 0x02,
|
||||
0x00, 80,
|
||||
0x00, 32
|
||||
};
|
||||
processor.processRecord(createPart);
|
||||
assertEquals(2, screen.getActivePartition());
|
||||
|
||||
// Now activate partition 0
|
||||
screen.setActivePartition(0);
|
||||
assertEquals(0, screen.getActivePartition());
|
||||
|
||||
// Send Outbound 3270DS targeting Partition 2 containing Write command to write 'H' at pos 0
|
||||
byte[] outboundDs = new byte[] {
|
||||
(byte) CMD_WSF,
|
||||
0x00, 0x0A, // Field length = 10
|
||||
(byte) SF_OUTBOUND_DS, 0x02, // Target PID = 2
|
||||
(byte) CMD_W, 0x00, // Write command with null WCC
|
||||
(byte) ORDER_SBA, 0x40, 0x40, // Pos 0
|
||||
(byte) 0xC8 // EBCDIC 'H'
|
||||
};
|
||||
|
||||
processor.processRecord(outboundDs);
|
||||
|
||||
// Verify active partition was switched to PID 2
|
||||
assertEquals(2, screen.getActivePartition());
|
||||
assertEquals('H', screen.getCell(0).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStructuredField0x0FSetWindow() {
|
||||
// SF 0x0F with explicit viewport window (xmin=10, ymin=20, xmax=500, ymax=300)
|
||||
byte[] setWindowSf = new byte[] {
|
||||
(byte) CMD_WSF,
|
||||
0x00, 0x0B, // Length = 11
|
||||
(byte) SF_SET_WINDOW,
|
||||
0x00, 10, // xMin = 10
|
||||
0x00, 20, // yMin = 20
|
||||
0x01, (byte) 0xF4, // xMax = 500
|
||||
0x01, 0x2C // yMax = 300
|
||||
};
|
||||
|
||||
processor.processRecord(setWindowSf);
|
||||
|
||||
// Test direct overload processSFSetWindow
|
||||
processor.processSFSetWindow(setWindowSf);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStructuredField0x0FDataUnitActivatesGraphicCursor() {
|
||||
assertFalse(processor.getGocaDecoder().isGraphicsCursorActive());
|
||||
|
||||
byte[] objDataSf = new byte[] {
|
||||
(byte) CMD_WSF,
|
||||
0x00, 0x04,
|
||||
(byte) SF_SET_WINDOW,
|
||||
(byte) GocaConstants.SF_OBJDATA_SUB // 0x0F
|
||||
};
|
||||
|
||||
processor.processRecord(objDataSf);
|
||||
assertTrue(processor.getGocaDecoder().isGraphicsCursorActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStructuredField0x20ObjectControl() {
|
||||
// SF 0x20 (3270 Graphics / Object Control) with GOCA begin/end area orders
|
||||
byte[] objControl = new byte[] {
|
||||
(byte) CMD_WSF,
|
||||
0x00, 0x07,
|
||||
(byte) SF_OBJECT_CONTROL,
|
||||
(byte) GocaConstants.G_GBAR, 0x00, // Begin area
|
||||
(byte) GocaConstants.G_GEAR, 0x00 // End area
|
||||
};
|
||||
|
||||
processor.processRecord(objControl);
|
||||
processor.processSFObjectControl(objControl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStructuredField0x24DocumentDataWithEmbeddedSCS() {
|
||||
AtomicReference<byte[]> scsCaptured = new AtomicReference<>();
|
||||
PrintSCS3270 mockScs = new PrintSCS3270(null, null, translator) {
|
||||
@Override
|
||||
public void processHostData(byte[] data, int offset, int length) {
|
||||
byte[] b = new byte[length];
|
||||
System.arraycopy(data, offset, b, 0, length);
|
||||
scsCaptured.set(b);
|
||||
}
|
||||
};
|
||||
processor.setEmbeddedScsProcessor(mockScs);
|
||||
|
||||
// SF 0x24 (Document Data) with 4 bytes of SCS printer data
|
||||
byte[] docData = new byte[] {
|
||||
(byte) CMD_WSF,
|
||||
0x00, 0x07,
|
||||
(byte) SF_DOCUMENT_DATA,
|
||||
(byte) 0x15, (byte) 0xC8, (byte) 0xC9, (byte) 0x15 // NL, 'H', 'I', NL
|
||||
};
|
||||
|
||||
processor.processRecord(docData);
|
||||
assertNotNull(scsCaptured.get());
|
||||
assertEquals(4, scsCaptured.get().length);
|
||||
assertEquals(0x15, scsCaptured.get()[0]);
|
||||
assertEquals((byte) 0xC8, scsCaptured.get()[1]);
|
||||
|
||||
// Test direct method
|
||||
processor.processSFDocumentData(docData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyBuilderGraphicColor109Bytes() {
|
||||
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||
byte[] grColor = qrBuilder.buildGraphicColor();
|
||||
|
||||
assertNotNull(grColor);
|
||||
assertEquals(105, grColor.length); // 105 bytes payload + 4 bytes header = 109 bytes in complete SF
|
||||
|
||||
// Check header bytes in payload: 0x00, 0x04, 0x00, 0xFF, 0xFF, 0x00, 0x10, 0x00, 0x10
|
||||
assertEquals(0x00, grColor[0]);
|
||||
assertEquals(0x04, grColor[1]);
|
||||
assertEquals(0x00, grColor[2]);
|
||||
assertEquals((byte) 0xFF, grColor[3]);
|
||||
assertEquals((byte) 0xFF, grColor[4]);
|
||||
assertEquals(0x00, grColor[5]);
|
||||
assertEquals(0x10, grColor[6]);
|
||||
assertEquals(0x00, grColor[7]);
|
||||
assertEquals(0x10, grColor[8]);
|
||||
|
||||
// Check aliases
|
||||
assertArrayEquals(grColor, qrBuilder.buildGrColor());
|
||||
assertArrayEquals(grColor, qrBuilder.buildGColor());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyBuilderAuxDeviceAndLineType() {
|
||||
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||
byte[] lineType = qrBuilder.buildAuxDevice();
|
||||
|
||||
assertNotNull(lineType);
|
||||
assertEquals(20, lineType.length); // 20 bytes payload + 4 bytes header = 24 bytes
|
||||
|
||||
assertArrayEquals(lineType, qrBuilder.buildLineType());
|
||||
assertArrayEquals(lineType, qrBuilder.buildAuxDev());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyBuilderOemFormatAndPort() {
|
||||
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||
byte[] port = qrBuilder.buildOemFormat();
|
||||
|
||||
assertNotNull(port);
|
||||
assertTrue(port.length > 0);
|
||||
assertArrayEquals(port, qrBuilder.buildPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyBuilderUsableAreaAspectRatiosAndNoArgOverloads() {
|
||||
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||
|
||||
// 1. Vector Graphics enabled (3179G standard: SDH = 16, flags = 0x03)
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||
byte[] uaVector = qrBuilder.buildUsableArea();
|
||||
assertNotNull(uaVector);
|
||||
assertEquals(19, uaVector.length);
|
||||
assertEquals(0x03, uaVector[0] & 0xFF); // Flags: 12/14 bit + Graphics
|
||||
assertEquals(9, uaVector[15] & 0xFF); // AW = 9
|
||||
assertEquals(16, uaVector[16] & 0xFF); // AH = 16
|
||||
|
||||
// 2. Text mode (GraphicsMode.NONE: SDH = 12, flags = 0x01)
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
|
||||
byte[] uaText = qrBuilder.buildUsableArea();
|
||||
assertNotNull(uaText);
|
||||
assertEquals(0x01, uaText[0] & 0xFF); // Flags: 12/14 bit only
|
||||
assertEquals(9, uaText[15] & 0xFF); // AW = 9
|
||||
assertEquals(12, uaText[16] & 0xFF); // AH = 12
|
||||
|
||||
// 3. No-arg overloads check
|
||||
assertNotNull(qrBuilder.buildSummary());
|
||||
assertNotNull(qrBuilder.buildAlphaPartitions());
|
||||
assertNotNull(qrBuilder.buildCharsets());
|
||||
assertNotNull(qrBuilder.buildColor());
|
||||
assertNotNull(qrBuilder.buildHighlighting());
|
||||
assertNotNull(qrBuilder.buildReplyModes());
|
||||
assertNotNull(qrBuilder.buildDdm());
|
||||
assertNotNull(qrBuilder.buildImplicitPartition());
|
||||
assertNotNull(qrBuilder.buildSegment());
|
||||
assertNotNull(qrBuilder.buildProcedure());
|
||||
assertNotNull(qrBuilder.buildGraphics());
|
||||
assertNotNull(qrBuilder.buildGImage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyBuilderColorTable18Bytes() {
|
||||
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||
byte[] colorTable = qrBuilder.buildColor();
|
||||
|
||||
assertNotNull(colorTable);
|
||||
assertEquals(18, colorTable.length, "Color table payload must be exactly 18 bytes matching HoD DS3270.java:1850");
|
||||
assertEquals(0x00, colorTable[0]);
|
||||
assertEquals(0x08, colorTable[1]);
|
||||
assertEquals(0x00, colorTable[2]);
|
||||
assertEquals((byte) 0xF4, colorTable[3]); // Default green
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDataStreamProcessorConvenienceOverloads() {
|
||||
// Test processEraseAllUnprotected
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.getCell(1).ucs4 = 'X';
|
||||
screen.getCell(1).ec = (byte) 0xE7;
|
||||
processor.processEraseAllUnprotected();
|
||||
assertEquals(0, screen.getCell(1).ucs4);
|
||||
|
||||
// Test processReadModified and processReadModifiedAll
|
||||
output.reset();
|
||||
processor.processReadModified();
|
||||
assertTrue(output.size() >= 3);
|
||||
|
||||
output.reset();
|
||||
processor.processReadModifiedAll();
|
||||
assertTrue(output.size() >= 3);
|
||||
|
||||
// Test processReadBuffer
|
||||
output.reset();
|
||||
processor.processReadBuffer();
|
||||
assertTrue(output.size() >= 3);
|
||||
|
||||
// Test processEraseWrite and processEraseWriteAlternate
|
||||
byte[] ewData = new byte[] { (byte) CMD_EW, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC1 };
|
||||
processor.processEraseWrite(ewData);
|
||||
assertEquals('A', screen.getCell(0).ucs4);
|
||||
|
||||
byte[] ewaData = new byte[] { (byte) CMD_EWA, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC2 };
|
||||
processor.processEraseWriteAlternate(ewaData);
|
||||
assertEquals('B', screen.getCell(0).ucs4);
|
||||
|
||||
// Test processSetReplyMode, processCreatePartition, processEraseReset byte[] overloads
|
||||
byte[] srm = new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_SET_REPLY_MODE, 0x00, (byte) SF_SRM_CHAR };
|
||||
processor.processSetReplyMode(srm);
|
||||
assertEquals((byte) SF_SRM_CHAR, screen.getReplyMode());
|
||||
|
||||
byte[] cp = new byte[] { (byte) CMD_WSF, 0x00, 0x08, (byte) SF_CREATE_PART, 0x01, 0x00, 80, 0x00, 24 };
|
||||
processor.processCreatePartition(cp);
|
||||
assertEquals(1, screen.getActivePartition());
|
||||
|
||||
byte[] er = new byte[] { (byte) CMD_WSF, 0x00, 0x04, (byte) SF_ERASE_RESET, (byte) SF_ER_DEFAULT };
|
||||
processor.processEraseReset(er);
|
||||
assertEquals(0, screen.getActivePartition());
|
||||
|
||||
// Test query partition overloads
|
||||
output.reset();
|
||||
processor.processSFReadPartitionQuery(new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QUERY });
|
||||
assertTrue(output.size() > 0);
|
||||
|
||||
output.reset();
|
||||
processor.processSFReadPartitionQueryList(new byte[] {
|
||||
(byte) CMD_WSF, 0x00, 0x07, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QLIST, (byte) SF_RPQ_LIST, (byte) QR_COLOR
|
||||
});
|
||||
assertTrue(output.size() > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class ECLOIAPhase3Test {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
private ECLOIA oia;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
oia = new ECLOIA(screen, input, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlphanumericTypeDetection() {
|
||||
screen.erase(false);
|
||||
// Unformatted screen
|
||||
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
|
||||
assertEquals("A", oia.getAlphanumericTypeString());
|
||||
|
||||
// Numeric field
|
||||
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_NUMERIC));
|
||||
screen.setCursorAddress(1);
|
||||
assertEquals(TYPE_NUMERIC, oia.getAlphanumericType());
|
||||
assertEquals("N", oia.getAlphanumericTypeString());
|
||||
|
||||
// Alphanumeric unprotected field
|
||||
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
|
||||
screen.setCursorAddress(11);
|
||||
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
|
||||
assertEquals("A", oia.getAlphanumericTypeString());
|
||||
|
||||
// DBCS cell
|
||||
ExtendedAttribute ea = screen.getCell(11);
|
||||
ea.cs = ExtendedAttribute.CS_DBCS;
|
||||
assertEquals(TYPE_DBCS, oia.getAlphanumericType());
|
||||
assertEquals("D", oia.getAlphanumericTypeString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOIAStatusFlagsAndStrings() {
|
||||
screen.erase(false);
|
||||
assertEquals("READY", oia.getStatusString());
|
||||
assertFalse(oia.isXSystem());
|
||||
assertFalse(oia.isXProt());
|
||||
assertFalse(oia.isXNum());
|
||||
assertFalse(oia.isXComm());
|
||||
assertFalse(oia.isXOverflow());
|
||||
assertFalse(oia.isXOperatorDue());
|
||||
|
||||
// Keyboard lock -> X-SYSTEM
|
||||
input.setKeyboardLocked(true);
|
||||
assertTrue(oia.isXSystem());
|
||||
assertTrue(oia.isXWait());
|
||||
assertEquals("X-SYSTEM", oia.getStatusString());
|
||||
input.setKeyboardLocked(false);
|
||||
|
||||
// Protected field inhibit
|
||||
oia.setInputInhibited(INHIBIT_PROTECTED_FIELD);
|
||||
assertTrue(oia.isXProt());
|
||||
assertEquals("X-PROT", oia.getStatusString());
|
||||
|
||||
// Numeric only inhibit
|
||||
oia.setInputInhibited(INHIBIT_NUMERIC_ONLY);
|
||||
assertTrue(oia.isXNum());
|
||||
assertEquals("X-NUM", oia.getStatusString());
|
||||
|
||||
// Overflow inhibit
|
||||
oia.setInputInhibited(INHIBIT_OVERFLOW);
|
||||
assertTrue(oia.isXOverflow());
|
||||
assertEquals("X-OVERFLOW", oia.getStatusString());
|
||||
|
||||
// Comm check inhibit
|
||||
oia.setInputInhibited(INHIBIT_COMM_CHECK);
|
||||
assertTrue(oia.isXComm());
|
||||
assertEquals("X-COMM", oia.getStatusString());
|
||||
|
||||
// Operator due inhibit
|
||||
oia.setInputInhibited(INHIBIT_OPERATOR_DUE);
|
||||
assertTrue(oia.isXOperatorDue());
|
||||
assertEquals("X-OP", oia.getStatusString());
|
||||
|
||||
// Insert mode
|
||||
oia.setInputInhibited(INHIBIT_NOT_INHIBITED);
|
||||
input.setInsertMode(true);
|
||||
assertTrue(oia.isXInsert());
|
||||
assertEquals("X-INSERT", oia.getStatusString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testECLFieldOperations() {
|
||||
screen.erase(false);
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setChar(0, 1, 'X');
|
||||
screen.setChar(0, 2, 'Y');
|
||||
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
ECLPS ps = new ECLPS(screen, input, translator);
|
||||
ECLField f = ps.getFieldList().getFirstField();
|
||||
assertNotNull(f);
|
||||
|
||||
assertEquals("XY ", f.getText());
|
||||
assertFalse(f.isModified());
|
||||
|
||||
f.setModified(true);
|
||||
assertTrue(f.isModified());
|
||||
|
||||
f.erase();
|
||||
assertEquals(" ", f.getText());
|
||||
assertFalse(f.isModified());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class ECLPSPhase3Test implements ECLConstants {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
private ECLPS ps;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
ps = new ECLPS(screen, input, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchPSOverloadsAndCaseOptions() {
|
||||
screen.erase(false);
|
||||
screen.setText("Host On-Demand ECL Automation");
|
||||
|
||||
// 1-based SearchPS
|
||||
assertEquals(6, ps.SearchPS("On-Demand"));
|
||||
assertEquals(6, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, true));
|
||||
assertEquals(0, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, false));
|
||||
assertEquals(0, ps.SearchPS("NONEXISTENT"));
|
||||
|
||||
// SearchPSExt
|
||||
int pos = ps.SearchPSExt("ECL", 1, 50, SEARCH_FORWARD, false, false);
|
||||
assertEquals(16, pos);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchStringBackwardsAndWrap() {
|
||||
screen.erase(false);
|
||||
screen.setChar(0, 10, 'A');
|
||||
screen.setChar(0, 11, 'B');
|
||||
screen.setChar(0, 12, 'C');
|
||||
|
||||
screen.setChar(1, 20, 'A');
|
||||
screen.setChar(1, 21, 'B');
|
||||
screen.setChar(1, 22, 'C');
|
||||
|
||||
// Backward search starting from row 1 col 0 (addr 80) should find "ABC" at row 0 col 10 (addr 10)
|
||||
int found = ps.searchString("ABC", 1, 0, SEARCH_BACKWARD, false);
|
||||
assertEquals(10, found);
|
||||
|
||||
// Backward search starting from row 0 col 5 (addr 5) wraps around and finds "ABC" at row 1 col 20 (addr 100)
|
||||
int wrapped = ps.searchString("ABC", 0, 5, SEARCH_BACKWARD, false);
|
||||
assertEquals(100, wrapped);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyStringRectangularExtraction() {
|
||||
screen.erase(false);
|
||||
// Row 0: "0123456789"
|
||||
for (int c = 0; c < 10; c++) {
|
||||
screen.setChar(0, c, (char) ('0' + c));
|
||||
}
|
||||
// Row 1: "ABCDEFGHIJ"
|
||||
for (int c = 0; c < 10; c++) {
|
||||
screen.setChar(1, c, (char) ('A' + c));
|
||||
}
|
||||
|
||||
// Copy columns 2..5 of rows 0..1
|
||||
String block = ps.copyString(0, 2, 1, 5);
|
||||
assertEquals("2345\nCDEF", block);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteStringRectangular() {
|
||||
screen.erase(false);
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
|
||||
|
||||
int pasted = ps.pasteString("HELLO\nWORLD", 0, 1);
|
||||
assertEquals(10, pasted);
|
||||
|
||||
assertEquals("HELLO", ps.getString(1, 5));
|
||||
assertEquals("WORLD", ps.getString(81, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendCharactersWithDelay() {
|
||||
screen.erase(false);
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
ps.sendCharacters("ABC[tab]DEF", 2);
|
||||
assertEquals('A', screen.getChar(0, 1));
|
||||
assertEquals('B', screen.getChar(0, 2));
|
||||
assertEquals('C', screen.getChar(0, 3));
|
||||
assertEquals('D', screen.getChar(0, 11));
|
||||
assertEquals('E', screen.getChar(0, 12));
|
||||
assertEquals('F', screen.getChar(0, 13));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWaitForScreenAndCursor() {
|
||||
screen.erase(false);
|
||||
assertFalse(ps.waitForScreen("READY", 50));
|
||||
assertFalse(ps.waitForCursor(5, 10, 50));
|
||||
|
||||
screen.setText("READY");
|
||||
assertTrue(ps.waitForScreen("READY", 50));
|
||||
assertTrue(ps.waitForScreen("READY", 0, 0, 50));
|
||||
assertFalse(ps.waitForScreen("READY", 1, 0, 50));
|
||||
|
||||
ps.setCursorPos(5, 10);
|
||||
assertTrue(ps.waitForCursor(5, 10, 50));
|
||||
}
|
||||
}
|
||||
@@ -114,4 +114,14 @@ public class ECLPSTest implements ECLConstants {
|
||||
int count = ps.pasteLineWrap("ABC\nDEF", 1, 80, false);
|
||||
assertEquals(6, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNVTModeAndSendKeys() {
|
||||
assertFalse(ps.isNVTmode());
|
||||
ps.setNVTmode(true);
|
||||
assertTrue(ps.isNVTmode());
|
||||
|
||||
ps.setNVTmode(false);
|
||||
assertFalse(ps.isNVTmode());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,199 @@ public class GocaDecoderPhase5Test {
|
||||
assertEquals(56, sf2.length);
|
||||
assertEquals(0x00, sf2[0]);
|
||||
assertEquals(0x34, sf2[1]);
|
||||
|
||||
byte[] sf3 = GraphicInputBuilder.buildPickCorrelation(120, 240, 5);
|
||||
assertEquals(56, sf3.length);
|
||||
assertEquals(120, ((sf3[24] & 0xFF) << 8) | (sf3[25] & 0xFF));
|
||||
assertEquals(240, ((sf3[26] & 0xFF) << 8) | (sf3[27] & 0xFF));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testViewingWindowClipping() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Define viewing window covering center of presentation space: [-100..100, -100..100]
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GSVW_DEF);
|
||||
out.write(0x08);
|
||||
out.write((byte) 0xFF); out.write((byte) 0x9C); // xMin = -100
|
||||
out.write((byte) 0xFF); out.write((byte) 0x9C); // yMin = -100
|
||||
out.write(0x00); out.write(100); // xMax = 100
|
||||
out.write(0x00); out.write(100); // yMax = 100
|
||||
|
||||
// Draw a line inside presentation space but outside the viewing window: (-300, -150) -> (-200, -150)
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
|
||||
out.write(GocaConstants.G_GLINE); out.write(0x08);
|
||||
out.write((byte) 0xFE); out.write((byte) 0xD4); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-300, -150)
|
||||
out.write((byte) 0xFF); out.write((byte) 0x38); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-200, -150)
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
// Pixels outside viewing window should have been clipped out
|
||||
int pxClipped = plane.mapX(-300);
|
||||
int pyClipped = plane.mapY(-150);
|
||||
int pixelOutside = plane.getRgbBuffer()[pyClipped * plane.getCanvasWidth() + pxClipped];
|
||||
assertEquals(0, pixelOutside, "Pixel outside viewing window must be 0 (clipped)");
|
||||
|
||||
// Now draw a line crossing the center (0, 0) -> (50, 50)
|
||||
ByteArrayOutputStream insideStream = new ByteArrayOutputStream();
|
||||
insideStream.write(GocaConstants.G_GLINE); insideStream.write(0x08);
|
||||
insideStream.write(0x00); insideStream.write(0); insideStream.write(0x00); insideStream.write(0);
|
||||
insideStream.write(0x00); insideStream.write(50); insideStream.write(0x00); insideStream.write(50);
|
||||
|
||||
byte[] inBytes = insideStream.toByteArray();
|
||||
decoder.decodeGoca(inBytes, 0, inBytes.length);
|
||||
|
||||
int pxInside = plane.mapX(0);
|
||||
int pyInside = plane.mapY(0);
|
||||
int pixelInside = plane.getRgbBuffer()[pyInside * plane.getCanvasWidth() + pxInside];
|
||||
assertNotEquals(0, pixelInside, "Pixel inside viewing window must be drawn");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFractionalLineWidth() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Fractional Line Width: 2.5 (0x02 0x80)
|
||||
out.write(GocaConstants.G_GSFLW);
|
||||
out.write(0x02);
|
||||
out.write(0x80);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertEquals(2.5, decoder.getFractionalLineWidth(), 0.01);
|
||||
assertEquals(2.5, plane.getFractionalLineWidth(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegmentCharacteristics() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Flags = 0xC0: Chained (0x80), Dynamic (0x40), Visible (0x00)
|
||||
decoder.processSegmentCharacteristics(new byte[]{(byte) 0xC0});
|
||||
assertTrue(decoder.isSegChained());
|
||||
assertTrue(decoder.isSegDynamic());
|
||||
assertTrue(decoder.isSegVisible());
|
||||
|
||||
// Flags = 0x20: Invisible
|
||||
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x20});
|
||||
assertFalse(decoder.isSegChained());
|
||||
assertFalse(decoder.isSegDynamic());
|
||||
assertFalse(decoder.isSegVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterAngleAndShear() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Character Angle: 45 degrees vector (ax=10, ay=10)
|
||||
out.write(GocaConstants.G_GSCA);
|
||||
out.write(0x04);
|
||||
out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(10);
|
||||
|
||||
// Set Character Shear: vector (sx=10, sy=0)
|
||||
out.write(GocaConstants.G_GSCR);
|
||||
out.write(0x04);
|
||||
out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(0);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeGoca(stream, 0, stream.length);
|
||||
|
||||
assertEquals(45.0, decoder.getCharAngle(), 0.01);
|
||||
assertEquals(90.0, decoder.getCharShear(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindingRuleAreaFill() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
FillArea fillArea = new FillArea();
|
||||
fillArea.setFillRule(GocaConstants.FILL_RULE_WINDING);
|
||||
assertEquals(GocaConstants.FILL_RULE_WINDING, fillArea.getFillRule());
|
||||
|
||||
// Fill self-intersecting bow-tie polygon
|
||||
int[] px = new int[]{50, 150, 50, 150};
|
||||
int[] py = new int[]{50, 150, 150, 50};
|
||||
fillArea.fill(plane, px, py, 4, 0xFF00FF00, GocaConstants.PT_SOLID, false, 0, 0, 0, 2, 0);
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiBitAndCompressedImages() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
|
||||
// Test 2-bit per pixel image
|
||||
byte[] twoBppData = new byte[]{(byte) 0b00011011}; // 4 pixels: 0, 1 (Blue), 2 (Red), 3 (Green)
|
||||
plane.drawImage(10, 10, 4, 1, twoBppData, 0xFFFFFFFF, GocaConstants.BPP_2, GocaConstants.IMG_UNCOMPRESSED);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
// Test 4-bit per pixel image
|
||||
byte[] fourBppData = new byte[]{(byte) 0x12, (byte) 0x34}; // 4 pixels: 1, 2, 3, 4
|
||||
plane.clear();
|
||||
plane.drawImage(10, 10, 4, 1, fourBppData, 0xFFFFFFFF, GocaConstants.BPP_4, GocaConstants.IMG_UNCOMPRESSED);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
// Test RLE decompressed image
|
||||
byte[] rleData = new byte[]{
|
||||
0x04, (byte) 0xFF // 4 repeats of 0xFF
|
||||
};
|
||||
byte[] decompressed = GraphicsPlane.decompressGocaRle(rleData, 32, 1, GocaConstants.BPP_1);
|
||||
assertEquals(4, decompressed.length);
|
||||
assertEquals((byte) 0xFF, decompressed[0]);
|
||||
assertEquals((byte) 0xFF, decompressed[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiColorProgrammedSymbolsOverlay() {
|
||||
ProgramSymbolManager psm = new ProgramSymbolManager(9, 16);
|
||||
|
||||
// Load Red plane (colorPlane = 1) into Slot 4 (LCID 0x41, Start 0x40, RWS 0x04)
|
||||
byte[] loadRed = new byte[4 + 6 + 18];
|
||||
loadRed[0] = (byte) 0x81; // Extended header present (0x80) | Format 1
|
||||
loadRed[1] = 0x41; // LCID
|
||||
loadRed[2] = 0x40; // Start codepoint
|
||||
loadRed[3] = 0x04; // RWS Slot 4 (Triple Plane)
|
||||
loadRed[4] = 0x06; // Ext header length = 6
|
||||
loadRed[5] = 0x00;
|
||||
loadRed[6] = 9; // Cell width = 9
|
||||
loadRed[7] = 16; // Cell height = 16
|
||||
loadRed[8] = 0x00;
|
||||
loadRed[9] = 0x01; // Plane = Red (1)
|
||||
for (int i = 0; i < 18; i++) loadRed[10 + i] = (byte) 0xFF;
|
||||
|
||||
psm.loadps(loadRed);
|
||||
|
||||
// Load Green plane (colorPlane = 2) into the same slot without clearing (clearAll = false)
|
||||
byte[] loadGreen = new byte[4 + 6 + 18];
|
||||
loadGreen[0] = (byte) 0x81;
|
||||
loadGreen[1] = 0x41;
|
||||
loadGreen[2] = 0x40;
|
||||
loadGreen[3] = 0x04;
|
||||
loadGreen[4] = 0x06;
|
||||
loadGreen[5] = 0x00;
|
||||
loadGreen[6] = 9;
|
||||
loadGreen[7] = 16;
|
||||
loadGreen[8] = 0x00;
|
||||
loadGreen[9] = 0x02; // Plane = Green (2)
|
||||
for (int i = 0; i < 18; i++) loadGreen[10 + i] = (byte) 0xFF;
|
||||
|
||||
psm.loadps(loadGreen);
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x41, 0x40);
|
||||
assertNotNull(slot);
|
||||
byte[] pixels = slot.getPixelData();
|
||||
assertNotNull(pixels);
|
||||
// Pixels should be composite of Red (1) | Green (2) = Yellow (3)
|
||||
assertEquals(3, pixels[0] & 0xFF);
|
||||
}
|
||||
|
||||
private static byte outCoord(int val) {
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class InputProcessorPhase4Test {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private ECLOIA oia;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
oia = new ECLOIA(screen, inputProcessor, null);
|
||||
inputProcessor.setOIA(oia);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumericOnlyValidation() {
|
||||
// Setup numeric field at pos 0 (FA), data at 1..9
|
||||
screen.setFieldAttribute(0, (byte) FA_NUMERIC);
|
||||
screen.setFieldAttribute(10, (byte) FA_PROTECT); // terminate field
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
// Typing valid digits and punctuation
|
||||
inputProcessor.typeCharacter('1');
|
||||
assertEquals(2, screen.getCursorAddress());
|
||||
assertEquals('1', (char) screen.getCell(1).ucs4);
|
||||
assertFalse(inputProcessor.isKeyboardLocked());
|
||||
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||
|
||||
inputProcessor.typeCharacter('-');
|
||||
assertEquals(3, screen.getCursorAddress());
|
||||
assertEquals('-', (char) screen.getCell(2).ucs4);
|
||||
|
||||
inputProcessor.typeCharacter('.');
|
||||
assertEquals(4, screen.getCursorAddress());
|
||||
assertEquals('.', (char) screen.getCell(3).ucs4);
|
||||
|
||||
inputProcessor.typeCharacter(' ');
|
||||
assertEquals(5, screen.getCursorAddress());
|
||||
|
||||
// Typing invalid non-numeric character 'A' in numeric field
|
||||
inputProcessor.typeCharacter('A');
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.getInputInhibited());
|
||||
assertEquals("X-NUM", oia.getStatusString());
|
||||
// Buffer position 5 should NOT have changed
|
||||
assertEquals(5, screen.getCursorAddress());
|
||||
assertEquals(0, screen.getCell(5).ec);
|
||||
|
||||
// Reset clears error and unlocks keyboard
|
||||
inputProcessor.reset();
|
||||
assertFalse(inputProcessor.isKeyboardLocked());
|
||||
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProtectedFieldInhibition() {
|
||||
// Setup protected field at pos 0, length 10
|
||||
screen.setFieldAttribute(0, (byte) FA_PROTECT);
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
// Attempting to type into protected field
|
||||
inputProcessor.typeCharacter('Z');
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
assertEquals(ECLConstants.INHIBIT_PROTECTED_FIELD, oia.getInputInhibited());
|
||||
assertEquals("X-PROT", oia.getStatusString());
|
||||
assertEquals(0, screen.getCell(1).ec);
|
||||
|
||||
// Reset unlocks
|
||||
inputProcessor.reset();
|
||||
assertFalse(inputProcessor.isKeyboardLocked());
|
||||
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoSkipFieldBoundary() {
|
||||
// Field 1: Unprotected at pos 0 (chars at 1, 2)
|
||||
screen.setFieldAttribute(0, (byte) 0x00);
|
||||
// Field 2: Auto-skip at pos 3 (FA_PROTECT | FA_NUMERIC) (chars at 4, 5)
|
||||
screen.setFieldAttribute(3, (byte) (FA_PROTECT | FA_NUMERIC));
|
||||
// Field 3: Unprotected at pos 6 (chars at 7, 8)
|
||||
screen.setFieldAttribute(6, (byte) 0x00);
|
||||
// Delimiter at pos 9
|
||||
screen.setFieldAttribute(9, (byte) FA_PROTECT);
|
||||
|
||||
// Cursor at pos 1, type 'A' -> moves to pos 2
|
||||
screen.setCursorAddress(1);
|
||||
inputProcessor.typeCharacter('A');
|
||||
assertEquals(2, screen.getCursorAddress());
|
||||
|
||||
// Type 'B' at pos 2 (the last char in Field 1) -> auto-skips over Field 2 (pos 3..5) to pos 7!
|
||||
inputProcessor.typeCharacter('B');
|
||||
assertEquals(7, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertModeFieldOverflowInhibition() {
|
||||
// Unprotected field at pos 0, size 3 (positions 1, 2, 3)
|
||||
screen.setFieldAttribute(0, (byte) 0x00);
|
||||
screen.setFieldAttribute(4, (byte) FA_PROTECT);
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
inputProcessor.typeCharacter('A');
|
||||
inputProcessor.typeCharacter('B');
|
||||
inputProcessor.typeCharacter('C');
|
||||
|
||||
// Field is full: pos 1='A', pos 2='B', pos 3='C'
|
||||
assertEquals('A', (char) screen.getCell(1).ucs4);
|
||||
assertEquals('B', (char) screen.getCell(2).ucs4);
|
||||
assertEquals('C', (char) screen.getCell(3).ucs4);
|
||||
|
||||
// Enable insert mode and attempt to type at pos 1
|
||||
inputProcessor.setInsertMode(true);
|
||||
screen.setCursorAddress(1);
|
||||
inputProcessor.typeCharacter('X');
|
||||
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
assertEquals(ECLConstants.INHIBIT_OVERFLOW, oia.getInputInhibited());
|
||||
assertEquals("X-OVERFLOW", oia.getStatusString());
|
||||
assertEquals('A', (char) screen.getCell(1).ucs4); // unchanged
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadModifiedInboundDataFraming() {
|
||||
// Create field at pos 0, type "TEST"
|
||||
screen.setFieldAttribute(0, (byte) 0x00);
|
||||
screen.setFieldAttribute(10, (byte) FA_PROTECT);
|
||||
screen.setCursorAddress(1);
|
||||
inputProcessor.typeCharacter('T');
|
||||
inputProcessor.typeCharacter('E');
|
||||
inputProcessor.typeCharacter('S');
|
||||
inputProcessor.typeCharacter('T');
|
||||
|
||||
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_ENTER, false);
|
||||
assertNotNull(inbound);
|
||||
assertTrue(inbound.length >= 7);
|
||||
assertEquals((byte) AID_ENTER, inbound[0]); // AID
|
||||
// Next 2 bytes: cursor address
|
||||
// Next byte: SBA (0x11)
|
||||
assertEquals((byte) ORDER_SBA, inbound[3]);
|
||||
// Followed by field address (pos 1), then EBCDIC bytes for "TEST"
|
||||
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[6]);
|
||||
assertEquals((byte) translator.unicodeToEbcdic('E'), inbound[7]);
|
||||
assertEquals((byte) translator.unicodeToEbcdic('S'), inbound[8]);
|
||||
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[9]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAidSelectDoesNotTransmitFieldCharacters() {
|
||||
// Create selectable field with designator ' ' at pos 1
|
||||
screen.setFieldAttribute(0, (byte) (FA_INT_HIGH_SEL | FA_NUMERIC)); // selectable
|
||||
screen.getCell(0).fa |= FA_MODIFY;
|
||||
screen.getCell(1).ec = 0x40; // space designator
|
||||
screen.getCell(2).ec = (byte) translator.unicodeToEbcdic('X');
|
||||
screen.setFieldAttribute(10, (byte) FA_PROTECT);
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_SELECT, false);
|
||||
assertNotNull(inbound);
|
||||
assertEquals((byte) AID_SELECT, inbound[0]);
|
||||
// Must contain SBA and designator address, but NO field text bytes
|
||||
assertEquals(6, inbound.length); // AID (1) + Cursor (2) + SBA (1) + Addr (2) = 6 bytes
|
||||
assertEquals((byte) ORDER_SBA, inbound[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPAKeysAndClearInboundData() {
|
||||
screen.setCursorAddress(80);
|
||||
byte[] pa1 = inputProcessor.buildReadModifiedInboundData(AID_PA1, false);
|
||||
assertEquals(3, pa1.length); // AID + 2 cursor bytes
|
||||
assertEquals((byte) AID_PA1, pa1[0]);
|
||||
|
||||
byte[] clear = inputProcessor.buildReadModifiedInboundData(AID_CLEAR, false);
|
||||
assertEquals(3, clear.length);
|
||||
assertEquals((byte) AID_CLEAR, clear[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBufferInboundDataStandardAndExtended() {
|
||||
screen.setFieldAttribute(0, (byte) 0x00);
|
||||
screen.getCell(1).ec = (byte) translator.unicodeToEbcdic('A');
|
||||
screen.setFieldAttribute(10, (byte) FA_PROTECT);
|
||||
|
||||
// Standard Field Mode
|
||||
screen.setReplyMode((byte) SF_SRM_FIELD);
|
||||
byte[] standardBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
|
||||
assertNotNull(standardBuf);
|
||||
assertEquals((byte) AID_ENTER, standardBuf[0]);
|
||||
assertEquals((byte) ORDER_SF, standardBuf[3]);
|
||||
|
||||
// Extended Field Mode
|
||||
screen.setReplyMode((byte) SF_SRM_XFIELD);
|
||||
byte[] extendedBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
|
||||
assertNotNull(extendedBuf);
|
||||
assertEquals((byte) AID_ENTER, extendedBuf[0]);
|
||||
assertEquals((byte) ORDER_SFE, extendedBuf[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWordLeftWordRightAndFieldEndNavigation() {
|
||||
// Field: "HELLO WORLD "
|
||||
screen.setFieldAttribute(0, (byte) 0x00);
|
||||
screen.setFieldAttribute(25, (byte) FA_PROTECT);
|
||||
|
||||
String text = "HELLO WORLD ";
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
screen.getCell(1 + i).ec = (byte) translator.unicodeToEbcdic(text.charAt(i));
|
||||
screen.getCell(1 + i).ucs4 = text.charAt(i);
|
||||
}
|
||||
|
||||
// Test Field End
|
||||
screen.setCursorAddress(1);
|
||||
inputProcessor.processFieldEnd();
|
||||
// Immediately following 'D' in WORLD (pos 1 + 13 = 14)
|
||||
assertEquals(14, screen.getCursorAddress());
|
||||
|
||||
// Test Word Left from pos 14 -> jumps to start of "WORLD" (pos 9)
|
||||
inputProcessor.processWordLeft();
|
||||
assertEquals(9, screen.getCursorAddress());
|
||||
|
||||
// Test Word Left from pos 9 -> jumps to start of "HELLO" (pos 1)
|
||||
inputProcessor.processWordLeft();
|
||||
assertEquals(1, screen.getCursorAddress());
|
||||
|
||||
// Test Word Right from pos 1 -> jumps to start of "WORLD" (pos 9)
|
||||
inputProcessor.processWordRight();
|
||||
assertEquals(9, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAll22CompatibleMethodsAndOverloads() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
// 1. processChar
|
||||
client.processChar('A');
|
||||
client.processChar('B', 5, false);
|
||||
|
||||
// 2. processEnter
|
||||
client.processEnter();
|
||||
|
||||
// 3. processPF
|
||||
client.processPF(3);
|
||||
|
||||
// 4. processPA
|
||||
client.processPA(1);
|
||||
|
||||
// 5. processClear
|
||||
client.processClear();
|
||||
|
||||
// 6-9. Cursor navigation
|
||||
client.processCursorUp();
|
||||
client.processCursorDown();
|
||||
client.processCursorLeft();
|
||||
client.processCursorRight();
|
||||
|
||||
// 10-13. Field navigation
|
||||
client.processTab();
|
||||
client.processBackTab();
|
||||
client.processHome();
|
||||
client.processNewline();
|
||||
|
||||
// 14-16. Editing
|
||||
client.processDelete();
|
||||
client.processBackspace();
|
||||
client.processEraseEOF();
|
||||
client.processEraseInput();
|
||||
|
||||
// 17-19. Special orders & toggles
|
||||
client.processDup();
|
||||
client.processFieldMark();
|
||||
client.processToggleInsert();
|
||||
|
||||
// 20-22. Control / Selection
|
||||
client.processReset();
|
||||
client.processWordLeft();
|
||||
client.processWordRight();
|
||||
client.processFieldEnd();
|
||||
client.processAttn();
|
||||
client.processSysReq();
|
||||
client.processCurSel();
|
||||
client.processLightPen();
|
||||
client.processLightPen(10);
|
||||
|
||||
assertNotNull(client.getInputProcessor().buildReadModifiedInboundData());
|
||||
assertNotNull(client.getInputProcessor().buildReadBufferInboundData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendKeysMnemonicTokens() {
|
||||
screen.setFieldAttribute(0, (byte) 0x00);
|
||||
screen.setFieldAttribute(15, (byte) 0x00);
|
||||
screen.setFieldAttribute(30, (byte) FA_PROTECT);
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
inputProcessor.sendKeys("123[tab]456[wordleft][fieldend][reset]");
|
||||
assertEquals('1', (char) screen.getCell(1).ucs4);
|
||||
assertEquals('2', (char) screen.getCell(2).ucs4);
|
||||
assertEquals('3', (char) screen.getCell(3).ucs4);
|
||||
assertEquals('4', (char) screen.getCell(16).ucs4);
|
||||
assertEquals('5', (char) screen.getCell(17).ucs4);
|
||||
assertEquals('6', (char) screen.getCell(18).ucs4);
|
||||
assertFalse(inputProcessor.isKeyboardLocked());
|
||||
}
|
||||
}
|
||||
@@ -453,17 +453,15 @@ public class InputProcessorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAidWhenGraphicsCursorActiveFraming() {
|
||||
public void testSendAidAlwaysSendsStandard3270StreamEvenIfGraphicCursorActive() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
screen.getCell(1).ec = (byte) 0xC1; // 'A'
|
||||
screen.setCellFA(5, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setCursorAddress(2);
|
||||
|
||||
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
|
||||
goca.setGraphicsCursorActive(true);
|
||||
goca.setGraphicCursorPosition(150, -80);
|
||||
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@@ -478,64 +476,10 @@ public class InputProcessorTest {
|
||||
|
||||
byte[] result = sent.get();
|
||||
assertNotNull(result);
|
||||
// Total expected length:
|
||||
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes
|
||||
assertEquals(64, result.length);
|
||||
assertEquals((byte) AID_SF, result[0]);
|
||||
// SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification
|
||||
assertEquals(0x00, result[1]);
|
||||
assertEquals(0x34, result[2]);
|
||||
// SF ID = 0x0F0F
|
||||
assertEquals(0x0F, result[3]);
|
||||
assertEquals(0x0F, result[4]);
|
||||
// Coordinates in SF at index 1 + 24 = 25
|
||||
int gx = (result[25] << 8) | (result[26] & 0xFF);
|
||||
int gy = (result[27] << 8) | (result[28] & 0xFF);
|
||||
assertEquals(150, (short) gx);
|
||||
assertEquals(-80, (short) gy);
|
||||
// Keyboard constants at index 1 + 31 = 32 and 1 + 33 = 34
|
||||
assertEquals(0x07, result[32]);
|
||||
assertEquals(0x07, result[34]);
|
||||
assertEquals((byte) 0xFF, result[35]);
|
||||
assertEquals((byte) AID_ENTER, result[36]);
|
||||
|
||||
// Trailing AID at index 57
|
||||
assertEquals((byte) AID_ENTER, result[57]);
|
||||
// Trailing SBA at 60
|
||||
assertEquals((byte) ORDER_SBA, result[60]);
|
||||
// Trailing field content 'A' at 63
|
||||
assertEquals((byte) 0xC1, result[63]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAidPAWhenGraphicsCursorActiveFraming() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
screen.getCell(1).ec = (byte) 0xC1;
|
||||
screen.setCursorAddress(2);
|
||||
|
||||
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
|
||||
goca.setGraphicsCursorActive(true);
|
||||
goca.setGraphicCursorPosition(100, 200);
|
||||
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
protected void sendAidResponse(byte[] data) {
|
||||
sent.set(data);
|
||||
}
|
||||
};
|
||||
input.setGocaDecoder(goca);
|
||||
|
||||
input.sendAid(AID_PA1);
|
||||
|
||||
byte[] result = sent.get();
|
||||
assertNotNull(result);
|
||||
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_PA1 0x6C) + 2 (Cursor Addr) = 60 bytes (no modified field data)
|
||||
assertEquals(60, result.length);
|
||||
assertEquals((byte) AID_SF, result[0]);
|
||||
assertEquals((byte) AID_PA1, result[36]); // Keyboard AID in SF
|
||||
assertEquals((byte) AID_PA1, result[57]); // Trailing AID
|
||||
// Standard 3270 stream: AID_ENTER (1) + Cursor Addr (2) + SBA (1) + Field Addr (2) + Data 'A' (1) = 7 bytes
|
||||
assertEquals(7, result.length);
|
||||
assertEquals((byte) AID_ENTER, result[0]);
|
||||
assertEquals((byte) ORDER_SBA, result[3]);
|
||||
assertEquals((byte) 0xC1, result[6]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,4 +138,125 @@ public class NvtProcessorTest {
|
||||
assertEquals('\r', (char) sent[5]);
|
||||
assertEquals('\n', (char) sent[6]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCursorPositionReportDSR() {
|
||||
// Move to row 12, col 34 (0-indexed: row 11, col 33)
|
||||
byte[] move = "\u001B[12;34H".getBytes();
|
||||
processor.processNVTData(move, 0, move.length);
|
||||
output.reset();
|
||||
|
||||
// Send DSR Cursor Position Request: ESC [ 6 n
|
||||
byte[] dsr = "\u001B[6n".getBytes();
|
||||
processor.processNVTData(dsr, 0, dsr.length);
|
||||
|
||||
assertEquals("\u001B[12;34R", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeviceAttributesDA() {
|
||||
output.reset();
|
||||
// Send DA Request: ESC [ c
|
||||
byte[] da1 = "\u001B[c".getBytes();
|
||||
processor.processNVTData(da1, 0, da1.length);
|
||||
assertEquals("\u001B[?1;2c", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
|
||||
output.reset();
|
||||
// Send DA Request: ESC [ 0 c
|
||||
byte[] da2 = "\u001B[0c".getBytes();
|
||||
processor.processNVTData(da2, 0, da2.length);
|
||||
assertEquals("\u001B[?1;2c", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtendedAnsiSequences() {
|
||||
// CHA: Cursor Horizontal Absolute -> ESC [ 20 G (moves to col 20 -> 0-indexed col 19)
|
||||
byte[] cha = "\u001B[20G".getBytes();
|
||||
processor.processNVTData(cha, 0, cha.length);
|
||||
assertEquals(19, screen.getCursorAddress());
|
||||
|
||||
// CNL: Cursor Next Line -> ESC [ 2 E (down 2 rows, col 0)
|
||||
byte[] cnl = "\u001B[2E".getBytes();
|
||||
processor.processNVTData(cnl, 0, cnl.length);
|
||||
assertEquals(2 * 80, screen.getCursorAddress());
|
||||
|
||||
// CPL: Cursor Previous Line -> ESC [ 1 F (up 1 row, col 0)
|
||||
byte[] cpl = "\u001B[1F".getBytes();
|
||||
processor.processNVTData(cpl, 0, cpl.length);
|
||||
assertEquals(1 * 80, screen.getCursorAddress());
|
||||
|
||||
// Write line "ABCDE"
|
||||
byte[] text = "\u001B[1;1HABCDE".getBytes();
|
||||
processor.processNVTData(text, 0, text.length);
|
||||
assertEquals('A', screen.getCell(0).ucs4);
|
||||
assertEquals('E', screen.getCell(4).ucs4);
|
||||
|
||||
// Move to pos 2 ('C') and ECH (Erase Character): ESC [ 2 X
|
||||
byte[] ech = "\u001B[1;3H\u001B[2X".getBytes();
|
||||
processor.processNVTData(ech, 0, ech.length);
|
||||
assertEquals('A', screen.getCell(0).ucs4);
|
||||
assertEquals('B', screen.getCell(1).ucs4);
|
||||
assertEquals(' ', screen.getCell(2).ucs4);
|
||||
assertEquals(' ', screen.getCell(3).ucs4);
|
||||
assertEquals('E', screen.getCell(4).ucs4);
|
||||
|
||||
// DCH: Delete Character at pos 0 -> ESC [ 1;1H ESC [ 1 P
|
||||
byte[] dch = "\u001B[1;1H\u001B[1P".getBytes();
|
||||
processor.processNVTData(dch, 0, dch.length);
|
||||
assertEquals('B', screen.getCell(0).ucs4);
|
||||
|
||||
// ICH: Insert Character at pos 0 -> ESC [ 1;1H ESC [ 1 @
|
||||
byte[] ich = "\u001B[1;1H\u001B[1@".getBytes();
|
||||
processor.processNVTData(ich, 0, ich.length);
|
||||
assertEquals(' ', screen.getCell(0).ucs4);
|
||||
assertEquals('B', screen.getCell(1).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScrollingRegionAndSpecialMovement() {
|
||||
// Set scrolling region lines 2..4 (1-indexed): ESC [ 2 ; 4 r
|
||||
byte[] decstbm = "\u001B[2;4r".getBytes();
|
||||
processor.processNVTData(decstbm, 0, decstbm.length);
|
||||
assertEquals(0, screen.getCursorAddress());
|
||||
|
||||
// Move to row 4 (bottom of region), col 1
|
||||
byte[] line4 = "\u001B[4;1HLine4".getBytes();
|
||||
processor.processNVTData(line4, 0, line4.length);
|
||||
assertEquals('L', screen.getCell(3 * 80).ucs4);
|
||||
|
||||
// Index (line feed down at bottom of region): ESC D -> scrolls region up
|
||||
byte[] ind = "\u001BD".getBytes();
|
||||
processor.processNVTData(ind, 0, ind.length);
|
||||
assertEquals('L', screen.getCell(2 * 80).ucs4); // Moved to row 3
|
||||
|
||||
// Reverse Index (line feed up at top of region): ESC [ 2;1H ESC M -> scrolls region down
|
||||
byte[] ri = "\u001B[2;1H\u001BM".getBytes();
|
||||
processor.processNVTData(ri, 0, ri.length);
|
||||
assertEquals('L', screen.getCell(3 * 80).ucs4); // Moved back down to row 4
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCursorVisibilityAndVt100Graphics() {
|
||||
assertTrue(processor.isCursorVisible());
|
||||
|
||||
// Hide cursor: ESC [ ? 25 l
|
||||
byte[] hide = "\u001B[?25l".getBytes();
|
||||
processor.processNVTData(hide, 0, hide.length);
|
||||
assertFalse(processor.isCursorVisible());
|
||||
|
||||
// Show cursor: ESC [ ? 25 h
|
||||
byte[] show = "\u001B[?25h".getBytes();
|
||||
processor.processNVTData(show, 0, show.length);
|
||||
assertTrue(processor.isCursorVisible());
|
||||
|
||||
// Enable VT100 line drawing on G0: ESC ( 0
|
||||
byte[] g0 = "\u001B(0q".getBytes();
|
||||
processor.processNVTData(g0, 0, g0.length);
|
||||
assertEquals('─', screen.getCell(0).ucs4);
|
||||
|
||||
// Reset G0 to ASCII: ESC ( B
|
||||
byte[] g0Ascii = "\u001B(Bq".getBytes();
|
||||
processor.processNVTData(g0Ascii, 0, g0Ascii.length);
|
||||
assertEquals('q', screen.getCell(1).ucs4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.AbstractDBCSCodePage;
|
||||
import haus.nightmare.lib3270j.charset.CodePageRegistry;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class ScreenBufferPhase3Test {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrappedFieldListFirstFaNotAtZero() {
|
||||
screen.erase(false);
|
||||
// Place FA at pos 100 (unprotected)
|
||||
screen.setFieldAttribute(100, (byte) FA_PRINTABLE);
|
||||
// Place FA at pos 150 (protected) - wraps around 1919 back to 99
|
||||
screen.setFieldAttribute(150, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
ECLFieldList list = screen.buildFieldList();
|
||||
assertEquals(2, list.getFieldCount());
|
||||
|
||||
ECLField f1 = list.getFirstField();
|
||||
assertNotNull(f1);
|
||||
assertEquals(100, f1.getStart());
|
||||
assertEquals(101, f1.getDataStart());
|
||||
assertEquals(149, f1.getEnd());
|
||||
assertEquals(49, f1.getLength());
|
||||
assertFalse(f1.isWrapped());
|
||||
assertFalse(f1.isProtected());
|
||||
|
||||
ECLField f2 = list.getNextField(f1);
|
||||
assertNotNull(f2);
|
||||
assertEquals(150, f2.getStart());
|
||||
assertEquals(151, f2.getDataStart());
|
||||
assertEquals(99, f2.getEnd());
|
||||
assertEquals(1920 - 150 - 1 + 100, f2.getLength());
|
||||
assertTrue(f2.isWrapped());
|
||||
assertTrue(f2.isProtected());
|
||||
|
||||
// Test field containment on wrapped field
|
||||
assertTrue(f2.contains(150));
|
||||
assertTrue(f2.contains(151));
|
||||
assertTrue(f2.contains(1919));
|
||||
assertTrue(f2.contains(0));
|
||||
assertTrue(f2.contains(99));
|
||||
assertFalse(f2.contains(100));
|
||||
assertFalse(f2.contains(120));
|
||||
|
||||
// Test field lookup at position
|
||||
assertEquals(150, screen.findFieldAt(50).getStart());
|
||||
assertEquals(100, screen.findFieldAt(120).getStart());
|
||||
|
||||
// Test navigation
|
||||
assertEquals(150, screen.findPrevField(120).getStart());
|
||||
assertEquals(150, screen.findNextField(120).getStart());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleFieldScreenWrap() {
|
||||
screen.erase(false);
|
||||
screen.setFieldAttribute(50, (byte) FA_PRINTABLE);
|
||||
|
||||
ECLFieldList list = screen.buildFieldList();
|
||||
assertEquals(1, list.getFieldCount());
|
||||
|
||||
ECLField f = list.getFirstField();
|
||||
assertNotNull(f);
|
||||
assertEquals(50, f.getStart());
|
||||
assertEquals(51, f.getDataStart());
|
||||
assertEquals(49, f.getEnd());
|
||||
assertEquals(1919, f.getLength());
|
||||
assertTrue(f.isWrapped());
|
||||
assertTrue(f.contains(0));
|
||||
assertTrue(f.contains(50));
|
||||
assertTrue(f.contains(1919));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertCharSBCSAndOverflow() {
|
||||
screen.erase(false);
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(6, (byte) (FA_PRINTABLE | FA_PROTECT)); // field of 5 chars (pos 1..5)
|
||||
|
||||
// Write 'A', 'B', 'C', 'D' at pos 1..4 (pos 5 is null)
|
||||
screen.setChar(0, 1, 'A');
|
||||
screen.setChar(0, 2, 'B');
|
||||
screen.setChar(0, 3, 'C');
|
||||
screen.setChar(0, 4, 'D');
|
||||
|
||||
// Insert 'Z' at pos 1
|
||||
boolean inserted = screen.insertChar(1, 'Z');
|
||||
assertTrue(inserted);
|
||||
assertEquals('Z', screen.getChar(0, 1));
|
||||
assertEquals('A', screen.getChar(0, 2));
|
||||
assertEquals('B', screen.getChar(0, 3));
|
||||
assertEquals('C', screen.getChar(0, 4));
|
||||
assertEquals('D', screen.getChar(0, 5));
|
||||
|
||||
// Field is now full (pos 5 is 'D'). Inserting another char should overflow and return false
|
||||
boolean overflow = screen.insertChar(1, 'X');
|
||||
assertFalse(overflow);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertAndShiftDBCSChar() {
|
||||
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
|
||||
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
|
||||
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
|
||||
|
||||
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
|
||||
dbcsScreen.erase(false);
|
||||
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Insert DBCS char at pos 1
|
||||
boolean ok = dbcsScreen.insertChar(1, '\u6771');
|
||||
assertTrue(ok);
|
||||
assertEquals(0x43, dbcsScreen.getCell(1).ec & 0xFF);
|
||||
assertEquals(0x41, dbcsScreen.getCell(2).ec & 0xFF);
|
||||
assertEquals(ExtendedAttribute.CS_DBCS, dbcsScreen.getCell(1).cs);
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
|
||||
assertEquals(3, dbcsScreen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteCharDBCSAndCleanAdjacentSISO() {
|
||||
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
|
||||
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
|
||||
cp.registerDbcsPair(0x4341, '\u6771');
|
||||
|
||||
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
|
||||
dbcsScreen.erase(false);
|
||||
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Insert DBCS character at pos 1
|
||||
dbcsScreen.insertChar(1, '\u6771');
|
||||
// Delete DBCS character at pos 1
|
||||
boolean deleted = dbcsScreen.deleteChar(1);
|
||||
assertTrue(deleted);
|
||||
assertEquals(0, dbcsScreen.getCell(1).ec);
|
||||
assertEquals(0, dbcsScreen.getCell(2).ec);
|
||||
|
||||
// Test orphaned SO/SI cleanup on delete
|
||||
dbcsScreen.setCell(3, 0x0E); // SO
|
||||
dbcsScreen.setCell(4, 0x43); // DBCS byte 1
|
||||
dbcsScreen.setCell(5, 0x41); // DBCS byte 2
|
||||
dbcsScreen.setCell(6, 0x0F); // SI
|
||||
dbcsScreen.getCell(4).cs = ExtendedAttribute.CS_DBCS;
|
||||
dbcsScreen.getCell(4).db = ExtendedAttribute.DB_LEFT;
|
||||
dbcsScreen.getCell(5).cs = ExtendedAttribute.CS_DBCS;
|
||||
dbcsScreen.getCell(5).db = ExtendedAttribute.DB_RIGHT;
|
||||
|
||||
// Deleting the DBCS character at pos 4 pulls SI (pos 6) adjacent to SO (pos 3), triggering SISO cleanup
|
||||
dbcsScreen.deleteChar(4);
|
||||
assertEquals(0, dbcsScreen.getCell(3).ec);
|
||||
assertEquals(0, dbcsScreen.getCell(4).ec);
|
||||
assertEquals(0, dbcsScreen.getCell(5).ec);
|
||||
assertEquals(0, dbcsScreen.getCell(6).ec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryAssistDOCModeAndTabStops() {
|
||||
screen.erase(false);
|
||||
assertFalse(screen.isEntryAssistDOCmode());
|
||||
assertFalse(screen.isEntryAssistWordWrap());
|
||||
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setEntryAssistWordWrap(true);
|
||||
screen.setEntryAssistStartColumn(5);
|
||||
screen.setEntryAssistEndColumn(75);
|
||||
screen.setEntryAssistTabStops(new int[]{ 10, 20, 30, 40 });
|
||||
|
||||
assertTrue(screen.isEntryAssistDOCmode());
|
||||
assertTrue(screen.isEntryAssistWordWrap());
|
||||
assertEquals(5, screen.getEntryAssistStartColumn());
|
||||
assertEquals(75, screen.getEntryAssistEndColumn());
|
||||
assertArrayEquals(new int[]{ 10, 20, 30, 40 }, screen.getEntryAssistTabStops());
|
||||
|
||||
// Test word tab forward with tab stops
|
||||
screen.setCursorPosition(0, 0);
|
||||
screen.processWordTab(true);
|
||||
assertEquals(10, screen.getCursorCol());
|
||||
|
||||
screen.processWordTab(true);
|
||||
assertEquals(20, screen.getCursorCol());
|
||||
|
||||
// Test word tab backward
|
||||
screen.processWordTab(false);
|
||||
assertEquals(10, screen.getCursorCol());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessDeleteWord() {
|
||||
screen.erase(false);
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Write "HELLO WORLD" starting at pos 1
|
||||
String text = "HELLO WORLD";
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
screen.setChar(0, 1 + i, text.charAt(i));
|
||||
}
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
screen.processDeleteWord();
|
||||
|
||||
// "HELLO " is deleted, "WORLD" is shifted left to pos 1
|
||||
assertEquals('W', screen.getChar(0, 1));
|
||||
assertEquals('O', screen.getChar(0, 2));
|
||||
assertEquals('R', screen.getChar(0, 3));
|
||||
assertEquals('L', screen.getChar(0, 4));
|
||||
assertEquals('D', screen.getChar(0, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessSOSIDisplayTransformation() {
|
||||
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
|
||||
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
|
||||
cp.registerDbcsPair(0x4341, '\u6771');
|
||||
|
||||
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
|
||||
dbcsScreen.erase(false);
|
||||
|
||||
// Put SO (0x0E), 0x43, 0x41, SI (0x0F) at pos 0..3
|
||||
dbcsScreen.setCell(0, 0x0E);
|
||||
dbcsScreen.setCell(1, 0x43);
|
||||
dbcsScreen.setCell(2, 0x41);
|
||||
dbcsScreen.setCell(3, 0x0F);
|
||||
|
||||
dbcsScreen.processSOSI();
|
||||
|
||||
assertEquals(ExtendedAttribute.DB_SO, dbcsScreen.getCell(0).db);
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
|
||||
assertEquals(ExtendedAttribute.DB_SI, dbcsScreen.getCell(3).db);
|
||||
assertEquals('\u6771', dbcsScreen.getCell(1).ucs4);
|
||||
assertEquals('\u6771', dbcsScreen.getCell(2).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessorsAndConvenienceMethods() {
|
||||
screen.erase(false);
|
||||
assertEquals(24 * 80, screen.getSize());
|
||||
|
||||
screen.writeChar(10, (byte) 0xC1); // 'A' in CP037
|
||||
assertEquals('A', screen.getChar(0, 10));
|
||||
|
||||
screen.setChar(1, 5, 'Z');
|
||||
assertEquals('Z', screen.getChar(1, 5));
|
||||
|
||||
ExtendedAttribute ea = new ExtendedAttribute();
|
||||
ea.fg = 2; // RED
|
||||
screen.setExtAttr(1, 5, ea);
|
||||
assertEquals(2, screen.getExtAttr(1, 5).fg);
|
||||
|
||||
screen.setText("TESTING 123");
|
||||
assertEquals("TESTING 123", screen.getString(0, 11));
|
||||
assertEquals(0, screen.searchString("TESTING"));
|
||||
assertEquals(-1, screen.searchString("NOTFOUND"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ProxyConnectionTest {
|
||||
|
||||
@Test
|
||||
public void testParseHostStringWithProxyFlags() {
|
||||
// HTTP proxy without auth
|
||||
ConnectionConfig c1 = ConnectionConfig.parseHostString("--proxy=http://proxy.corp.com:8080 mainframe.net:23", 23, TerminalModel.IBM_3279_4);
|
||||
assertEquals(ConnectionConfig.ProxyType.HTTP, c1.getProxyType());
|
||||
assertEquals("proxy.corp.com", c1.getProxyHost());
|
||||
assertEquals(8080, c1.getProxyPort());
|
||||
assertNull(c1.getProxyUsername());
|
||||
assertEquals("mainframe.net", c1.getHost());
|
||||
assertEquals(23, c1.getPort());
|
||||
|
||||
// HTTP proxy with auth
|
||||
ConnectionConfig c2 = ConnectionConfig.parseHostString("--proxy=http://alice:secret123@10.0.0.1:3128 zos.ibm.com:2323", 23, TerminalModel.IBM_3279_4);
|
||||
assertEquals(ConnectionConfig.ProxyType.HTTP, c2.getProxyType());
|
||||
assertEquals("10.0.0.1", c2.getProxyHost());
|
||||
assertEquals(3128, c2.getProxyPort());
|
||||
assertEquals("alice", c2.getProxyUsername());
|
||||
assertEquals("secret123", c2.getProxyPassword());
|
||||
assertEquals("zos.ibm.com", c2.getHost());
|
||||
assertEquals(2323, c2.getPort());
|
||||
|
||||
// SOCKS4 proxy
|
||||
ConnectionConfig c3 = ConnectionConfig.parseHostString("--proxy=socks4://socks.local:1080 L:secure.mvs.com:992", 23, TerminalModel.IBM_3279_4);
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS4, c3.getProxyType());
|
||||
assertEquals("socks.local", c3.getProxyHost());
|
||||
assertEquals(1080, c3.getProxyPort());
|
||||
assertTrue(c3.isUseTls());
|
||||
assertEquals("secure.mvs.com", c3.getHost());
|
||||
assertEquals(992, c3.getPort());
|
||||
|
||||
// SOCKS5 proxy with auth
|
||||
ConnectionConfig c4 = ConnectionConfig.parseHostString("--proxy=socks5://bob:pass55@127.0.0.1:9050 P:vm.host:23", 23, TerminalModel.IBM_3279_4);
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, c4.getProxyType());
|
||||
assertEquals("127.0.0.1", c4.getProxyHost());
|
||||
assertEquals(9050, c4.getProxyPort());
|
||||
assertEquals("bob", c4.getProxyUsername());
|
||||
assertEquals("pass55", c4.getProxyPassword());
|
||||
assertFalse(c4.isTn3270eEnabled());
|
||||
assertEquals("vm.host", c4.getHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpProxyConnectHandshake() throws Exception {
|
||||
try (ServerSocket proxyServer = new ServerSocket(0)) {
|
||||
int proxyPort = proxyServer.getLocalPort();
|
||||
|
||||
CountDownLatch serverHandshakeDone = new CountDownLatch(1);
|
||||
CompletableFuture<String> receivedRequest = new CompletableFuture<>();
|
||||
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try (Socket clientSock = proxyServer.accept()) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSock.getInputStream(), StandardCharsets.US_ASCII));
|
||||
StringBuilder req = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) break;
|
||||
req.append(line).append("\n");
|
||||
}
|
||||
receivedRequest.complete(req.toString());
|
||||
|
||||
// Respond 200 Connection established
|
||||
OutputStream out = clientSock.getOutputStream();
|
||||
out.write("HTTP/1.1 200 Connection established\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
|
||||
out.flush();
|
||||
serverHandshakeDone.countDown();
|
||||
|
||||
// Echo back test data
|
||||
int b;
|
||||
while ((b = clientSock.getInputStream().read()) != -1) {
|
||||
out.write(b);
|
||||
out.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
receivedRequest.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
serverThread.setDaemon(true);
|
||||
serverThread.start();
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("target.mainframe.org", 23);
|
||||
config.setProxy(ConnectionConfig.ProxyType.HTTP, "127.0.0.1", proxyPort, "testuser", "testpass");
|
||||
config.setConnectTimeoutMs(5000);
|
||||
|
||||
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
|
||||
TelnetConnection connection = new TelnetConnection(config, fsm);
|
||||
|
||||
connection.connect();
|
||||
assertTrue(connection.isConnected());
|
||||
|
||||
String req = receivedRequest.get(3, TimeUnit.SECONDS);
|
||||
assertTrue(req.startsWith("CONNECT target.mainframe.org:23 HTTP/1.1"), "Expected CONNECT request");
|
||||
assertTrue(req.contains("Proxy-Authorization: Basic "), "Expected Basic auth in proxy request");
|
||||
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSocks4ProxyHandshake() throws Exception {
|
||||
try (ServerSocket proxyServer = new ServerSocket(0)) {
|
||||
int proxyPort = proxyServer.getLocalPort();
|
||||
CountDownLatch handshakeDone = new CountDownLatch(1);
|
||||
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try (Socket clientSock = proxyServer.accept()) {
|
||||
InputStream in = clientSock.getInputStream();
|
||||
OutputStream out = clientSock.getOutputStream();
|
||||
|
||||
// Read SOCKS4 request header
|
||||
byte[] req = new byte[8];
|
||||
in.read(req);
|
||||
assertEquals(0x04, req[0]); // SOCKS4
|
||||
assertEquals(0x01, req[1]); // CONNECT
|
||||
|
||||
// Read null-terminated username
|
||||
ByteArrayOutputStream userBuf = new ByteArrayOutputStream();
|
||||
int b;
|
||||
while ((b = in.read()) != 0 && b != -1) {
|
||||
userBuf.write(b);
|
||||
}
|
||||
assertEquals("user4", new String(userBuf.toByteArray()));
|
||||
|
||||
// Reply 0x00 0x5A (Request granted)
|
||||
byte[] resp = new byte[] { 0x00, 0x5A, 0x00, 0x17, 127, 0, 0, 1 };
|
||||
out.write(resp);
|
||||
out.flush();
|
||||
handshakeDone.countDown();
|
||||
|
||||
while (in.read() != -1) {}
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.setDaemon(true);
|
||||
serverThread.start();
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", 23);
|
||||
config.setProxy(ConnectionConfig.ProxyType.SOCKS4, "127.0.0.1", proxyPort, "user4", null);
|
||||
config.setConnectTimeoutMs(5000);
|
||||
|
||||
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
|
||||
TelnetConnection connection = new TelnetConnection(config, fsm);
|
||||
|
||||
connection.connect();
|
||||
assertTrue(connection.isConnected());
|
||||
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
|
||||
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSocks5ProxyHandshakeWithAuth() throws Exception {
|
||||
try (ServerSocket proxyServer = new ServerSocket(0)) {
|
||||
int proxyPort = proxyServer.getLocalPort();
|
||||
CountDownLatch handshakeDone = new CountDownLatch(1);
|
||||
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try (Socket clientSock = proxyServer.accept()) {
|
||||
InputStream in = clientSock.getInputStream();
|
||||
OutputStream out = clientSock.getOutputStream();
|
||||
|
||||
// 1. Read method selection
|
||||
int ver = in.read();
|
||||
int nmethods = in.read();
|
||||
byte[] methods = new byte[nmethods];
|
||||
in.read(methods);
|
||||
assertEquals(0x05, ver);
|
||||
|
||||
// Select USER_PASS (0x02)
|
||||
out.write(new byte[] { 0x05, 0x02 });
|
||||
out.flush();
|
||||
|
||||
// 2. Read auth request (RFC 1929)
|
||||
int authVer = in.read();
|
||||
int ulen = in.read();
|
||||
byte[] u = new byte[ulen];
|
||||
in.read(u);
|
||||
int plen = in.read();
|
||||
byte[] p = new byte[plen];
|
||||
in.read(p);
|
||||
|
||||
assertEquals(1, authVer);
|
||||
assertEquals("admin", new String(u));
|
||||
assertEquals("pass123", new String(p));
|
||||
|
||||
// Auth success: 0x01 0x00
|
||||
out.write(new byte[] { 0x01, 0x00 });
|
||||
out.flush();
|
||||
|
||||
// 3. Read connect command
|
||||
byte[] cmd = new byte[4];
|
||||
in.read(cmd);
|
||||
assertEquals(0x05, cmd[0]);
|
||||
assertEquals(0x01, cmd[1]); // CONNECT
|
||||
int atyp = cmd[3];
|
||||
if (atyp == 0x03) { // Domain name
|
||||
int dlen = in.read();
|
||||
byte[] d = new byte[dlen];
|
||||
in.read(d);
|
||||
} else if (atyp == 0x01) { // IPv4
|
||||
in.read(new byte[4]);
|
||||
}
|
||||
in.read(new byte[2]); // Port
|
||||
|
||||
// Reply success: 0x05 0x00 0x00 0x01 127.0.0.1:port
|
||||
out.write(new byte[] { 0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0, 23 });
|
||||
out.flush();
|
||||
handshakeDone.countDown();
|
||||
|
||||
while (in.read() != -1) {}
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.setDaemon(true);
|
||||
serverThread.start();
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("mvs.corp.local", 23);
|
||||
config.setProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", proxyPort, "admin", "pass123");
|
||||
config.setConnectTimeoutMs(5000);
|
||||
|
||||
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
|
||||
TelnetConnection connection = new TelnetConnection(config, fsm);
|
||||
|
||||
connection.connect();
|
||||
assertTrue(connection.isConnected());
|
||||
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
|
||||
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.SCSInboundListener;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
|
||||
|
||||
public class TelnetFSMPhase1FullTest {
|
||||
|
||||
private ConnectionConfig config;
|
||||
private ScreenBuffer screenBuffer;
|
||||
private DataStreamProcessor dsProcessor;
|
||||
private InputProcessor inputProcessor;
|
||||
private TelnetFSM fsm;
|
||||
private MockConnection connection;
|
||||
private EbcdicTranslator translator;
|
||||
|
||||
private static class MockConnection extends TelnetConnection {
|
||||
final List<byte[]> sentData = new ArrayList<>();
|
||||
boolean tlsUpgraded = false;
|
||||
|
||||
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
super(config, fsm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void sendRaw(byte[] data) {
|
||||
sentData.add(data.clone());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void sendRaw(byte[] data, int offset, int length) {
|
||||
byte[] b = new byte[length];
|
||||
System.arraycopy(data, offset, b, 0, length);
|
||||
sentData.add(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void upgradeToTls() throws IOException {
|
||||
tlsUpgraded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
translator = new EbcdicTranslator();
|
||||
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
dsProcessor.setInputProcessor(inputProcessor);
|
||||
connection = new MockConnection(config, fsm);
|
||||
fsm.setConnection(connection);
|
||||
}
|
||||
|
||||
private void feedBytes(int... bytes) {
|
||||
for (int b : bytes) {
|
||||
fsm.feedByte(b & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendTn3270eResponseAndHoD5ByteSignature() {
|
||||
connection.sentData.clear();
|
||||
|
||||
// 1. Standard 6-byte RFC 2355 response
|
||||
fsm.sendTn3270eResponse((byte) RSF_POSITIVE_RESPONSE, (byte) POS_DEVICE_END, 0x1234);
|
||||
assertEquals(1, connection.sentData.size());
|
||||
byte[] resp6 = connection.sentData.get(0);
|
||||
assertEquals(6 + 2, resp6.length); // 6 bytes header/trailer + IAC EOR
|
||||
assertEquals(DT_RESPONSE, resp6[0] & 0xFF);
|
||||
assertEquals(0, resp6[1] & 0xFF);
|
||||
assertEquals(RSF_POSITIVE_RESPONSE, resp6[2] & 0xFF);
|
||||
assertEquals(0x12, resp6[3] & 0xFF);
|
||||
assertEquals(0x34, resp6[4] & 0xFF);
|
||||
assertEquals(POS_DEVICE_END, resp6[5] & 0xFF);
|
||||
assertEquals(IAC, resp6[6] & 0xFF);
|
||||
assertEquals(EOR, resp6[7] & 0xFF);
|
||||
|
||||
// 2. HoD 5-byte send_response
|
||||
connection.sentData.clear();
|
||||
fsm.send_response((short) 0, (short) RSF_POSITIVE_RESPONSE, 0x5678);
|
||||
assertEquals(1, connection.sentData.size());
|
||||
byte[] resp5 = connection.sentData.get(0);
|
||||
assertEquals(5 + 2, resp5.length); // 5 bytes + IAC EOR
|
||||
assertEquals(DT_RESPONSE, resp5[0] & 0xFF);
|
||||
assertEquals(0, resp5[1] & 0xFF);
|
||||
assertEquals(RSF_POSITIVE_RESPONSE, resp5[2] & 0xFF);
|
||||
assertEquals(0x56, resp5[3] & 0xFF);
|
||||
assertEquals(0x78, resp5[4] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartTlsOption46NegotiationAndElevation() {
|
||||
config.setStartTlsEnabled(true);
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// Server sends DO STARTTLS (Option 46)
|
||||
feedBytes(IAC, DO, TELOPT_STARTTLS);
|
||||
|
||||
// Client must reply WILL STARTTLS
|
||||
assertEquals(1, connection.sentData.size());
|
||||
assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_STARTTLS }, connection.sentData.get(0));
|
||||
assertTrue(fsm.getMyOpts()[TELOPT_STARTTLS]);
|
||||
connection.sentData.clear();
|
||||
|
||||
// Server sends SB STARTTLS 1 (TLS_FOLLOWS) SE
|
||||
feedBytes(IAC, SB, TELOPT_STARTTLS, TLS_FOLLOWS, IAC, SE);
|
||||
|
||||
// Connection must have been upgraded to TLS
|
||||
assertTrue(connection.tlsUpgraded, "Expected upgradeToTls() to be invoked on STARTTLS_FOLLOWS");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRfc1572NewEnvironEmptySendReturnsAllVariables() {
|
||||
config.setEnvironmentVariable("USER", "MAINFRAME_USER");
|
||||
config.setEnvironmentVariable("SYSTEMTYPE", "MVS");
|
||||
config.setUserVariable("IBM_EXPRESS_LOGON", "CERT_AUTH_ENABLED");
|
||||
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// Server sends DO NEW_ENVIRON
|
||||
feedBytes(IAC, DO, TELOPT_NEW_ENVIRON);
|
||||
assertEquals(1, connection.sentData.size());
|
||||
assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_NEW_ENVIRON }, connection.sentData.get(0));
|
||||
connection.sentData.clear();
|
||||
|
||||
// Server sends SB NEW-ENVIRON SEND SE (empty query -> all variables)
|
||||
feedBytes(IAC, SB, TELOPT_NEW_ENVIRON, TELQUAL_SEND, IAC, SE);
|
||||
|
||||
assertEquals(1, connection.sentData.size());
|
||||
byte[] pkt = connection.sentData.get(0);
|
||||
assertTrue(pkt.length >= 6);
|
||||
assertEquals(IAC, pkt[0] & 0xFF);
|
||||
assertEquals(SB, pkt[1] & 0xFF);
|
||||
assertEquals(TELOPT_NEW_ENVIRON, pkt[2] & 0xFF);
|
||||
assertEquals(TELQUAL_IS, pkt[3] & 0xFF);
|
||||
|
||||
String pktStr = new String(pkt);
|
||||
assertTrue(pktStr.contains("USER"));
|
||||
assertTrue(pktStr.contains("MAINFRAME_USER"));
|
||||
assertTrue(pktStr.contains("SYSTEMTYPE"));
|
||||
assertTrue(pktStr.contains("MVS"));
|
||||
assertTrue(pktStr.contains("IBM_EXPRESS_LOGON"));
|
||||
assertTrue(pktStr.contains("CERT_AUTH_ENABLED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRfc1572NewEnvironSelectiveSend() throws Exception {
|
||||
config.setEnvironmentVariable("USER", "ALICE");
|
||||
config.setEnvironmentVariable("JOB", "DAILY_REPORT");
|
||||
config.setUserVariable("SEC_TOKEN", "XYZ123");
|
||||
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// Server sends SB NEW-ENVIRON SEND VAR "USER" USERVAR "SEC_TOKEN" SE
|
||||
ByteArrayOutputStream query = new ByteArrayOutputStream();
|
||||
query.write(IAC);
|
||||
query.write(SB);
|
||||
query.write(TELOPT_NEW_ENVIRON);
|
||||
query.write(TELQUAL_SEND);
|
||||
query.write(TELOBJ_VAR);
|
||||
query.write("USER".getBytes());
|
||||
query.write(TELOBJ_USERVAR);
|
||||
query.write("SEC_TOKEN".getBytes());
|
||||
query.write(IAC);
|
||||
query.write(SE);
|
||||
|
||||
for (byte b : query.toByteArray()) {
|
||||
fsm.feedByte(b & 0xFF);
|
||||
}
|
||||
|
||||
assertEquals(1, connection.sentData.size());
|
||||
String respStr = new String(connection.sentData.get(0));
|
||||
assertTrue(respStr.contains("USER"));
|
||||
assertTrue(respStr.contains("ALICE"));
|
||||
assertTrue(respStr.contains("SEC_TOKEN"));
|
||||
assertTrue(respStr.contains("XYZ123"));
|
||||
assertFalse(respStr.contains("JOB"), "Unrequested variable JOB should not be sent");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendNewEnvironmentVariablesFromProperties() {
|
||||
Properties props = new Properties();
|
||||
props.setProperty("VAR_TEST", "VAL1");
|
||||
props.setProperty("USERVAR_TOKEN", "VAL2");
|
||||
|
||||
connection.sentData.clear();
|
||||
fsm.sendNewEnvironmentVariables(props);
|
||||
|
||||
assertEquals(1, connection.sentData.size());
|
||||
byte[] pkt = connection.sentData.get(0);
|
||||
assertEquals(IAC, pkt[0] & 0xFF);
|
||||
assertEquals(SB, pkt[1] & 0xFF);
|
||||
assertEquals(TELOPT_NEW_ENVIRON, pkt[2] & 0xFF);
|
||||
assertEquals(TELQUAL_IS, pkt[3] & 0xFF);
|
||||
|
||||
String pktStr = new String(pkt);
|
||||
assertTrue(pktStr.contains("VAR_TEST"));
|
||||
assertTrue(pktStr.contains("VAL1"));
|
||||
assertTrue(pktStr.contains("USERVAR_TOKEN"));
|
||||
assertTrue(pktStr.contains("VAL2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInboundSCSDataHandlingAndAutoPositiveResponse() throws Exception {
|
||||
config.setTn3270eEnabled(true);
|
||||
fsm.onConnected();
|
||||
|
||||
// Negotiate TN3270E with RESPONSES and SCS_CTL_CODES
|
||||
feedBytes(IAC, DO, TELOPT_TN3270E);
|
||||
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
|
||||
byte[] devTypeIs = new byte[]{
|
||||
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
|
||||
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '8', '7', '-', '1',
|
||||
(byte) IAC, (byte) SE
|
||||
};
|
||||
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||
|
||||
byte[] funcsReq = new byte[]{
|
||||
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
|
||||
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
|
||||
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SCS_CTL_CODES,
|
||||
(byte) IAC, (byte) SE
|
||||
};
|
||||
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||
|
||||
List<byte[]> scsReceived = new ArrayList<>();
|
||||
fsm.addSCSInboundListener(new SCSInboundListener() {
|
||||
@Override
|
||||
public void onSCSDataReceived(byte[] data, int offset, int length) {
|
||||
byte[] copy = new byte[length];
|
||||
System.arraycopy(data, offset, copy, 0, length);
|
||||
scsReceived.add(copy);
|
||||
}
|
||||
});
|
||||
|
||||
connection.sentData.clear();
|
||||
|
||||
// Send DT_SCS_DATA (0x01) record with ALWAYS_RESPONSE
|
||||
int seq = 0x00A1;
|
||||
byte[] scsPayload = new byte[] { 0x15, 0x2B, (byte) 0xD2, 0x04, 0x15 }; // SCS orders
|
||||
ByteArrayOutputStream scsRecord = new ByteArrayOutputStream();
|
||||
scsRecord.write(DT_SCS_DATA);
|
||||
scsRecord.write(0x00); // Request flag
|
||||
scsRecord.write(RSF_ALWAYS_RESPONSE);
|
||||
scsRecord.write((seq >> 8) & 0xFF);
|
||||
scsRecord.write(seq & 0xFF);
|
||||
scsRecord.write(scsPayload);
|
||||
scsRecord.write(IAC);
|
||||
scsRecord.write(EOR);
|
||||
|
||||
for (byte b : scsRecord.toByteArray()) {
|
||||
fsm.feedByte(b & 0xFF);
|
||||
}
|
||||
|
||||
// Verify SCS listener was notified with exact payload
|
||||
assertEquals(1, scsReceived.size());
|
||||
assertArrayEquals(scsPayload, scsReceived.get(0));
|
||||
|
||||
// Verify positive response was sent
|
||||
boolean foundPositive = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
if (pkt.length >= 8 &&
|
||||
(pkt[0] & 0xFF) == DT_RESPONSE &&
|
||||
(pkt[2] & 0xFF) == RSF_POSITIVE_RESPONSE &&
|
||||
((((pkt[3] & 0xFF) << 8) | (pkt[4] & 0xFF)) == seq) &&
|
||||
(pkt[5] & 0xFF) == POS_DEVICE_END) {
|
||||
foundPositive = true;
|
||||
}
|
||||
}
|
||||
assertTrue(foundPositive, "Expected positive response for DT_SCS_DATA with RSF_ALWAYS_RESPONSE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessSysReqMethod() {
|
||||
config.setTn3270eEnabled(true);
|
||||
fsm.onConnected();
|
||||
|
||||
// Negotiate TN3270E
|
||||
feedBytes(IAC, DO, TELOPT_TN3270E);
|
||||
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
|
||||
byte[] devTypeIs = new byte[]{
|
||||
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
|
||||
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||
(byte) IAC, (byte) SE
|
||||
};
|
||||
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||
byte[] funcsReq = new byte[]{
|
||||
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
|
||||
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
|
||||
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SYSREQ,
|
||||
(byte) IAC, (byte) SE
|
||||
};
|
||||
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||
|
||||
// Bind session
|
||||
byte[] bindPacket = new byte[EH_SIZE + 35];
|
||||
bindPacket[0] = DT_BIND_IMAGE;
|
||||
bindPacket[1] = 0; bindPacket[2] = 0; bindPacket[3] = 0; bindPacket[4] = 1;
|
||||
bindPacket[EH_SIZE + 24] = 0x02;
|
||||
ByteArrayOutputStream bStream = new ByteArrayOutputStream();
|
||||
bStream.write(bindPacket, 0, bindPacket.length);
|
||||
bStream.write(IAC);
|
||||
bStream.write(EOR);
|
||||
for (byte b : bStream.toByteArray()) fsm.feedByte(b & 0xFF);
|
||||
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
|
||||
connection.sentData.clear();
|
||||
|
||||
// Call processSysReq()
|
||||
fsm.processSysReq();
|
||||
|
||||
// State changes to CONNECTED_SSCP and IAC AO sent
|
||||
assertEquals(ConnectionState.CONNECTED_SSCP, fsm.getConnectionState());
|
||||
boolean foundAo = false;
|
||||
for (byte[] pkt : connection.sentData) {
|
||||
if (pkt.length == 2 && (pkt[0] & 0xFF) == IAC && (pkt[1] & 0xFF) == AO) {
|
||||
foundAo = true;
|
||||
}
|
||||
}
|
||||
assertTrue(foundAo, "Expected IAC AO out-of-band telnet abort");
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,9 @@ public class TelnetFSMTest {
|
||||
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
|
||||
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
|
||||
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
haus.nightmare.lib3270j.input.InputProcessor inputProcessor =
|
||||
new haus.nightmare.lib3270j.input.InputProcessor(screenBuffer, new EbcdicTranslator(), fsm);
|
||||
dsProcessor.setInputProcessor(inputProcessor);
|
||||
connection = new MockConnection(config, fsm);
|
||||
fsm.setConnection(connection);
|
||||
}
|
||||
@@ -294,4 +297,212 @@ public class TelnetFSMTest {
|
||||
String resp3 = new String(lastPkt, 4, lastPkt.length - 6);
|
||||
assertEquals("IBM-3278-4-E", resp3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTn3270eNvtInboundAndOutbound() throws Exception {
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// Negotiate TN3270E without BIND-IMAGE
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
|
||||
|
||||
// Host sends DEVICE-TYPE SEND
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
|
||||
0x08, 0x01, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
|
||||
// Host sends DEVICE-TYPE IS IBM-3279-4-E
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
|
||||
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||
TelnetConstants.IAC, TelnetConstants.SE);
|
||||
|
||||
// Host sends FUNCTIONS IS (no BIND-IMAGE)
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
|
||||
0x03, 0x04, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
|
||||
|
||||
// Host sends DT_NVT_DATA (0x05) record with text "PROMPT: "
|
||||
byte[] nvtRecord = {
|
||||
0x05, // DT_NVT_DATA
|
||||
0x00, // request flag
|
||||
0x00, // response flag
|
||||
0x00, 0x01, // seq 1
|
||||
'P', 'R', 'O', 'M', 'P', 'T', ':', ' ',
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
|
||||
};
|
||||
for (byte b : nvtRecord) {
|
||||
fsm.feedByte(b & 0xFF);
|
||||
}
|
||||
|
||||
// FSM must transition to CONNECTED_E_NVT
|
||||
assertEquals(ConnectionState.CONNECTED_E_NVT, fsm.getConnectionState());
|
||||
assertTrue(fsm.getConnectionState().isNvt());
|
||||
|
||||
// Verify screen buffer contains the NVT text
|
||||
assertEquals('P', screenBuffer.getCell(0).ucs4);
|
||||
assertEquals('R', screenBuffer.getCell(1).ucs4);
|
||||
assertEquals('O', screenBuffer.getCell(2).ucs4);
|
||||
assertEquals('M', screenBuffer.getCell(3).ucs4);
|
||||
assertEquals('P', screenBuffer.getCell(4).ucs4);
|
||||
assertEquals('T', screenBuffer.getCell(5).ucs4);
|
||||
assertEquals(':', screenBuffer.getCell(6).ucs4);
|
||||
assertEquals(' ', screenBuffer.getCell(7).ucs4);
|
||||
assertEquals(8, screenBuffer.getCursorAddress());
|
||||
|
||||
// Test Outbound NVT sending in TN3270E mode
|
||||
connection.sentData.clear();
|
||||
fsm.sendNVTString("OK\n");
|
||||
|
||||
assertFalse(connection.sentData.isEmpty());
|
||||
byte[] sentPkt = connection.sentData.get(connection.sentData.size() - 1);
|
||||
|
||||
// Sent packet should be TN3270E record: 5-byte header + "OK\r\n" + IAC EOR
|
||||
assertEquals(5 + 4 + 2, sentPkt.length);
|
||||
assertEquals(0x05, sentPkt[0]); // DT_NVT_DATA
|
||||
assertEquals('O', sentPkt[5]);
|
||||
assertEquals('K', sentPkt[6]);
|
||||
assertEquals('\r', sentPkt[7]);
|
||||
assertEquals('\n', sentPkt[8]);
|
||||
assertEquals((byte) TelnetConstants.IAC, sentPkt[9]);
|
||||
assertEquals((byte) TelnetConstants.EOR, sentPkt[10]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainTelnetNvtStreamingAndLocalEcho() throws Exception {
|
||||
fsm.onConnected();
|
||||
config.setNvtLocalEcho(true);
|
||||
|
||||
// Receiving raw bytes in TELNET_PENDING automatically transitions to CONNECTED_NVT
|
||||
feedBytes('L', 'O', 'G', 'I', 'N', '>');
|
||||
assertEquals(ConnectionState.CONNECTED_NVT, fsm.getConnectionState());
|
||||
assertEquals('L', screenBuffer.getCell(0).ucs4);
|
||||
assertEquals('>', screenBuffer.getCell(5).ucs4);
|
||||
|
||||
// Sending NVT data with local echo enabled
|
||||
connection.sentData.clear();
|
||||
fsm.sendNVTString("TEST");
|
||||
|
||||
// Raw bytes sent over TCP
|
||||
assertEquals(1, connection.sentData.size());
|
||||
byte[] sent = connection.sentData.get(0);
|
||||
assertArrayEquals("TEST".getBytes(), sent);
|
||||
|
||||
// Local echo placed "TEST" on screen starting at address 6
|
||||
assertEquals('T', screenBuffer.getCell(6).ucs4);
|
||||
assertEquals('E', screenBuffer.getCell(7).ucs4);
|
||||
assertEquals('S', screenBuffer.getCell(8).ucs4);
|
||||
assertEquals('T', screenBuffer.getCell(9).ucs4);
|
||||
assertEquals(10, screenBuffer.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVmConmode3270TransitionTn3270e() throws Exception {
|
||||
fsm.onConnected();
|
||||
connection.sentData.clear();
|
||||
|
||||
// 1. Negotiate TN3270E
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
|
||||
0x08, 0x01, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
|
||||
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||
TelnetConstants.IAC, TelnetConstants.SE);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
|
||||
0x03, 0x04, TelnetConstants.IAC, TelnetConstants.SE);
|
||||
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
|
||||
|
||||
// 2. VM console in line-mode sends DT_NVT_DATA (e.g. CP prompt)
|
||||
byte[] nvtRecord = {
|
||||
0x05, 0x00, 0x00, 0x00, 0x01,
|
||||
'C', 'P', ' ', 'R', 'E', 'A', 'D', 'Y', '\r', '\n',
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
|
||||
};
|
||||
for (byte b : nvtRecord) fsm.feedByte(b & 0xFF);
|
||||
|
||||
assertEquals(ConnectionState.CONNECTED_E_NVT, fsm.getConnectionState());
|
||||
assertEquals(TelnetFSM.TN3270ESubmode.E_NVT, fsm.getTn3270eSubmode());
|
||||
|
||||
// 3. VM executes "cp term conmode 3270" and sends 3270 full screen (DT_3270_DATA)
|
||||
EbcdicTranslator trans = new EbcdicTranslator();
|
||||
String banner = "z/VM 3270 FULLSCREEN";
|
||||
byte[] stream3270 = new byte[5 + 5 + banner.length() + 2];
|
||||
// 5-byte TN3270E header
|
||||
stream3270[0] = 0x00; // DT_3270_DATA
|
||||
stream3270[1] = 0x00;
|
||||
stream3270[2] = 0x00;
|
||||
stream3270[3] = 0x00; stream3270[4] = 0x02; // seq 2
|
||||
// 3270 Orders
|
||||
stream3270[5] = (byte) 0xF5; // EraseWrite
|
||||
stream3270[6] = (byte) 0xC3; // WCC
|
||||
stream3270[7] = 0x11; // SBA
|
||||
stream3270[8] = 0x40; stream3270[9] = 0x40; // pos 0
|
||||
for (int i = 0; i < banner.length(); i++) {
|
||||
stream3270[10 + i] = (byte) trans.unicodeToEbcdic(banner.charAt(i));
|
||||
}
|
||||
stream3270[stream3270.length - 2] = (byte) TelnetConstants.IAC;
|
||||
stream3270[stream3270.length - 1] = (byte) TelnetConstants.EOR;
|
||||
|
||||
for (byte b : stream3270) fsm.feedByte(b & 0xFF);
|
||||
|
||||
// Verify transition from E_NVT to E_3270 (CONNECTED_TN3270E)
|
||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
|
||||
assertEquals(TelnetFSM.TN3270ESubmode.E_3270, fsm.getTn3270eSubmode());
|
||||
assertFalse(fsm.getConnectionState().isNvt());
|
||||
|
||||
// Verify screen displays 3270 content
|
||||
assertEquals('z', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
|
||||
assertEquals('/', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
|
||||
assertEquals('V', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
|
||||
assertEquals('M', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
|
||||
|
||||
// 4. Outbound 3270 AID transmission should now use DT_3270_DATA
|
||||
connection.sentData.clear();
|
||||
dsProcessor.getInputProcessor().sendAid(0x7D);
|
||||
|
||||
assertFalse(connection.sentData.isEmpty());
|
||||
byte[] sentAid = connection.sentData.get(connection.sentData.size() - 1);
|
||||
assertEquals(0x00, sentAid[0]); // DT_3270_DATA header
|
||||
assertEquals((byte) 0x7D, sentAid[5]); // Enter AID code
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVmConmode3270TransitionPlainTelnet() throws Exception {
|
||||
fsm.onConnected();
|
||||
config.setTn3270eEnabled(false);
|
||||
|
||||
// 1. Connected in line-mode (NVT)
|
||||
feedBytes('C', 'P', '>');
|
||||
assertEquals(ConnectionState.CONNECTED_NVT, fsm.getConnectionState());
|
||||
|
||||
// 2. VM sends BINARY and EOR options upon CONMODE 3270
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
|
||||
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
|
||||
|
||||
// Verify transition to CONNECTED_3270
|
||||
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
|
||||
|
||||
// 3. VM sends 3270 data stream
|
||||
EbcdicTranslator trans = new EbcdicTranslator();
|
||||
String msg = "VM 3270";
|
||||
byte[] rawStream = new byte[5 + msg.length() + 2];
|
||||
rawStream[0] = (byte) 0xF5; // EraseWrite
|
||||
rawStream[1] = (byte) 0xC3; // WCC
|
||||
rawStream[2] = 0x11; // SBA
|
||||
rawStream[3] = 0x40; rawStream[4] = 0x40;
|
||||
for (int i = 0; i < msg.length(); i++) {
|
||||
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
|
||||
}
|
||||
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
|
||||
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
|
||||
|
||||
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
|
||||
|
||||
assertEquals('V', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
|
||||
assertEquals('M', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user