This commit is contained in:
@@ -1,17 +1,20 @@
|
|||||||
package haus.nightmare.j3270;
|
package haus.nightmare.j3270;
|
||||||
|
|
||||||
import haus.nightmare.lib3270j.*;
|
import haus.nightmare.lib3270j.*;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||||
|
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import haus.nightmare.j3270.ui.StatusBar;
|
import haus.nightmare.j3270.ui.*;
|
||||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
|
||||||
import haus.nightmare.j3270.ft.FileTransfer;
|
import haus.nightmare.j3270.ft.FileTransfer;
|
||||||
import haus.nightmare.j3270.ft.FileTransferDialog;
|
import haus.nightmare.j3270.ft.FileTransferDialog;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.datatransfer.DataFlavor;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
|
import java.awt.print.PrinterJob;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.logging.*;
|
import java.util.logging.*;
|
||||||
|
|
||||||
@@ -36,6 +39,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
private FileTransfer fileTransfer;
|
private FileTransfer fileTransfer;
|
||||||
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
|
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||||
|
|
||||||
|
// Dialog instances
|
||||||
|
private FindDialog findDialog;
|
||||||
|
private ScriptDialog scriptDialog;
|
||||||
|
private FieldInspectorDialog fieldInspectorDialog;
|
||||||
|
private PrinterSessionDialog printerSessionDialog;
|
||||||
|
|
||||||
public J3270App() {
|
public J3270App() {
|
||||||
super("j3270 — Java TN3270 Terminal Emulator");
|
super("j3270 — Java TN3270 Terminal Emulator");
|
||||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
@@ -52,7 +61,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
refreshTimer = new Timer(100, e -> {
|
refreshTimer = new Timer(100, e -> {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
statusBar.updateStatus();
|
statusBar.updateStatus();
|
||||||
// Keep focus on terminal panel when window is active
|
|
||||||
if (isActive() && !terminalPanel.hasFocus()) {
|
if (isActive() && !terminalPanel.hasFocus()) {
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
@@ -66,12 +74,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
disconnect();
|
disconnect();
|
||||||
if (refreshTimer != null)
|
if (refreshTimer != null)
|
||||||
refreshTimer.stop();
|
refreshTimer.stop();
|
||||||
|
if (printerSessionDialog != null) {
|
||||||
|
printerSessionDialog.dispose();
|
||||||
|
}
|
||||||
terminalPanel.dispose();
|
terminalPanel.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void windowActivated(WindowEvent e) {
|
public void windowActivated(WindowEvent e) {
|
||||||
// When window gains focus, push to terminal panel
|
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,11 +89,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
private void buildUI() {
|
private void buildUI() {
|
||||||
terminalPanel = new TerminalPanel();
|
terminalPanel = new TerminalPanel();
|
||||||
|
|
||||||
// Status bar
|
|
||||||
statusBar = new StatusBar();
|
statusBar = new StatusBar();
|
||||||
|
|
||||||
// Layout
|
|
||||||
getContentPane().setLayout(new BorderLayout());
|
getContentPane().setLayout(new BorderLayout());
|
||||||
getContentPane().setBackground(new Color(10, 10, 10));
|
getContentPane().setBackground(new Color(10, 10, 10));
|
||||||
getContentPane().add(terminalPanel, BorderLayout.CENTER);
|
getContentPane().add(terminalPanel, BorderLayout.CENTER);
|
||||||
@@ -95,11 +102,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
menuBar.setBackground(new Color(30, 30, 30));
|
menuBar.setBackground(new Color(30, 30, 30));
|
||||||
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
|
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
|
||||||
|
|
||||||
// File menu
|
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||||
|
|
||||||
|
// 1. File menu
|
||||||
JMenu fileMenu = createMenu("File");
|
JMenu fileMenu = createMenu("File");
|
||||||
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
||||||
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
||||||
fileMenu.addSeparator();
|
fileMenu.addSeparator();
|
||||||
|
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
|
||||||
|
fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog));
|
||||||
|
fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen));
|
||||||
|
fileMenu.addSeparator();
|
||||||
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
|
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
|
||||||
fileMenu.addSeparator();
|
fileMenu.addSeparator();
|
||||||
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
||||||
@@ -108,7 +121,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}));
|
}));
|
||||||
menuBar.add(fileMenu);
|
menuBar.add(fileMenu);
|
||||||
|
|
||||||
// View menu
|
// 2. Edit menu
|
||||||
|
JMenu editMenu = createMenu("Edit");
|
||||||
|
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
|
||||||
|
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
|
||||||
|
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
|
||||||
|
editMenu.addSeparator();
|
||||||
|
editMenu.add(createMenuItem("Select All", KeyEvent.VK_A, () -> terminalPanel.selectAll()));
|
||||||
|
editMenu.add(createMenuItem("Clear Selection", -1, () -> terminalPanel.clearSelection()));
|
||||||
|
editMenu.addSeparator();
|
||||||
|
editMenu.add(createMenuItem("Find on Screen...", KeyEvent.VK_F, this::showFindDialog));
|
||||||
|
editMenu.add(createMenuItem("Find Next", KeyEvent.VK_G, this::findNext));
|
||||||
|
editMenu.add(createMenuItem("Find Previous", KeyEvent.VK_G, this::findPrevious, true));
|
||||||
|
menuBar.add(editMenu);
|
||||||
|
|
||||||
|
// 3. View menu
|
||||||
JMenu viewMenu = createMenu("View");
|
JMenu viewMenu = createMenu("View");
|
||||||
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
|
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
|
||||||
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
|
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
|
||||||
@@ -116,10 +143,67 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
terminalPanel.setFontSize(16);
|
terminalPanel.setFontSize(16);
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
}));
|
}));
|
||||||
|
viewMenu.addSeparator();
|
||||||
|
|
||||||
|
// CodePage Submenu
|
||||||
|
JMenu cpMenu = createMenu("Code Page");
|
||||||
|
String[] codePages = {
|
||||||
|
"037 - US / Canada / Brazil",
|
||||||
|
"1047 - IBM Open Systems / z/OS Unix",
|
||||||
|
"500 - International Latin-1",
|
||||||
|
"273 - Germany / Austria",
|
||||||
|
"277 - Denmark / Norway",
|
||||||
|
"278 - Sweden / Finland",
|
||||||
|
"280 - Italy",
|
||||||
|
"284 - Spain / Latin America",
|
||||||
|
"285 - United Kingdom",
|
||||||
|
"297 - France",
|
||||||
|
"870 - Eastern Europe / Latin-2",
|
||||||
|
"871 - Iceland",
|
||||||
|
"875 - Greece (Greek)",
|
||||||
|
"1026 - Turkey (Turkish)",
|
||||||
|
"1140 - US / Canada (Euro \u20AC)",
|
||||||
|
"1141 - Germany / Austria (Euro \u20AC)",
|
||||||
|
"1148 - International (Euro \u20AC)",
|
||||||
|
"930 - Japanese Katakana Mixed DBCS",
|
||||||
|
"939 - Japanese Latin Mixed DBCS",
|
||||||
|
"935 - Simplified Chinese Mixed DBCS",
|
||||||
|
"937 - Traditional Chinese Mixed DBCS",
|
||||||
|
"933 - Korean Mixed DBCS"
|
||||||
|
};
|
||||||
|
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));
|
||||||
|
cpMi.addActionListener(e -> changeCodePage(cpId));
|
||||||
|
cpMenu.add(cpMi);
|
||||||
|
}
|
||||||
|
viewMenu.add(cpMenu);
|
||||||
|
|
||||||
|
// Graphics Mode Submenu
|
||||||
|
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));
|
||||||
|
gmMi.addActionListener(e -> changeGraphicsMode(gm));
|
||||||
|
gfxMenu.add(gmMi);
|
||||||
|
}
|
||||||
|
viewMenu.add(gfxMenu);
|
||||||
|
|
||||||
|
viewMenu.addSeparator();
|
||||||
|
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
|
||||||
menuBar.add(viewMenu);
|
menuBar.add(viewMenu);
|
||||||
|
|
||||||
// Actions menu
|
// 4. Actions menu
|
||||||
JMenu actionsMenu = createMenu("Actions");
|
JMenu actionsMenu = createMenu("Actions");
|
||||||
|
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
|
||||||
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
|
client.sendEnter();
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
}));
|
||||||
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
|
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
|
||||||
if (client != null)
|
if (client != null)
|
||||||
client.sendClear();
|
client.sendClear();
|
||||||
@@ -130,17 +214,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
client.reset();
|
client.reset();
|
||||||
terminalPanel.repaint();
|
terminalPanel.repaint();
|
||||||
}));
|
}));
|
||||||
|
actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> {
|
||||||
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
|
client.eraseInput();
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
}, true));
|
||||||
|
actionsMenu.add(createMenuItem("Attention", KeyEvent.VK_A, () -> {
|
||||||
|
if (client != null && client.isConnected()) client.attn();
|
||||||
|
}, true));
|
||||||
|
actionsMenu.add(createMenuItem("System Request", KeyEvent.VK_S, () -> {
|
||||||
|
if (client != null && client.isConnected()) {
|
||||||
|
client.sysReq();
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
}, true));
|
||||||
|
actionsMenu.add(createMenuItem("Cursor Select", KeyEvent.VK_Q, () -> {
|
||||||
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
|
client.cursorSelect();
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
}, true));
|
||||||
actionsMenu.addSeparator();
|
actionsMenu.addSeparator();
|
||||||
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
||||||
terminalPanel.toggleLightPen();
|
terminalPanel.toggleLightPen();
|
||||||
}, true));
|
}, true));
|
||||||
actionsMenu.addSeparator();
|
actionsMenu.addSeparator();
|
||||||
|
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
||||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
||||||
menuBar.add(actionsMenu);
|
menuBar.add(actionsMenu);
|
||||||
|
|
||||||
// Help menu
|
// 5. Help menu
|
||||||
JMenu helpMenu = createMenu("Help");
|
JMenu helpMenu = createMenu("Help");
|
||||||
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
|
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
|
||||||
|
helpMenu.add(createMenuItem("Mnemonic Keystroke Reference", -1, this::showMnemonicHelp));
|
||||||
|
helpMenu.addSeparator();
|
||||||
helpMenu.add(createMenuItem("About", -1, this::showAbout));
|
helpMenu.add(createMenuItem("About", -1, this::showAbout));
|
||||||
menuBar.add(helpMenu);
|
menuBar.add(helpMenu);
|
||||||
|
|
||||||
@@ -169,6 +277,164 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
return createMenuItem(name, mnemonic, action, false);
|
return createMenuItem(name, mnemonic, action, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Dialog Openers ==========
|
||||||
|
|
||||||
|
private void showFindDialog() {
|
||||||
|
if (client == null) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Connect to a host before using Find.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (findDialog == null) {
|
||||||
|
findDialog = new FindDialog(this, client, terminalPanel);
|
||||||
|
}
|
||||||
|
findDialog.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findNext() {
|
||||||
|
if (client == null) return;
|
||||||
|
String last = FindDialog.getLastSearchText();
|
||||||
|
if (last == null || last.isEmpty()) {
|
||||||
|
showFindDialog();
|
||||||
|
} else {
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int curAddr = sb.getCursorAddress();
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
int curRow = curAddr / cols;
|
||||||
|
int curCol = (curAddr % cols + 1) % cols;
|
||||||
|
if (curCol == 0) curRow = (curRow + 1) % sb.getDisplayRows();
|
||||||
|
|
||||||
|
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_FORWARD, !FindDialog.getLastMatchCase());
|
||||||
|
if (found >= 0) {
|
||||||
|
sb.setCursorAddress(found);
|
||||||
|
terminalPanel.setSearchHighlight(found, last.length());
|
||||||
|
terminalPanel.repaint();
|
||||||
|
} else {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findPrevious() {
|
||||||
|
if (client == null) return;
|
||||||
|
String last = FindDialog.getLastSearchText();
|
||||||
|
if (last == null || last.isEmpty()) {
|
||||||
|
showFindDialog();
|
||||||
|
} else {
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int curAddr = sb.getCursorAddress();
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
int curRow = curAddr / cols;
|
||||||
|
int curCol = (curAddr % cols - 1 + cols) % cols;
|
||||||
|
if (curCol == cols - 1) curRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
|
||||||
|
|
||||||
|
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_BACKWARD, !FindDialog.getLastMatchCase());
|
||||||
|
if (found >= 0) {
|
||||||
|
sb.setCursorAddress(found);
|
||||||
|
terminalPanel.setSearchHighlight(found, last.length());
|
||||||
|
terminalPanel.repaint();
|
||||||
|
} else {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showScriptDialog() {
|
||||||
|
if (client == null) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Connect to a host before running scripts.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scriptDialog == null) {
|
||||||
|
scriptDialog = new ScriptDialog(this, client, terminalPanel);
|
||||||
|
}
|
||||||
|
scriptDialog.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showFieldInspectorDialog() {
|
||||||
|
if (client == null) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fieldInspectorDialog == null) {
|
||||||
|
fieldInspectorDialog = new FieldInspectorDialog(this, client, terminalPanel);
|
||||||
|
} else {
|
||||||
|
fieldInspectorDialog.refreshFields();
|
||||||
|
}
|
||||||
|
fieldInspectorDialog.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPrinterSessionDialog() {
|
||||||
|
if (printerSessionDialog == null) {
|
||||||
|
String host = (client != null) ? client.getConfig().getHost() : lastHost;
|
||||||
|
int port = (client != null) ? client.getConfig().getPort() : lastPort;
|
||||||
|
boolean tls = (client != null) ? client.getConfig().isUseTls() : false;
|
||||||
|
printerSessionDialog = new PrinterSessionDialog(this, host, port, tls);
|
||||||
|
}
|
||||||
|
printerSessionDialog.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPasteLineWrapDialog() {
|
||||||
|
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Clipboard is empty.", "Clipboard", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cols = client.getScreenBuffer().getDisplayCols();
|
||||||
|
String endColStr = JOptionPane.showInputDialog(this, "Enter right margin column (1-" + cols + ") for wrapping [0 = no right margin]:", "80");
|
||||||
|
if (endColStr != null) {
|
||||||
|
int endCol = 0;
|
||||||
|
try {
|
||||||
|
endCol = Integer.parseInt(endColStr.trim());
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
terminalPanel.pasteLineWrap(text, endCol, true);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Could not read clipboard: " + ex.getMessage(), "Paste Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeCodePage(String cpId) {
|
||||||
|
if (client != null) {
|
||||||
|
client.setCodePage(cpId);
|
||||||
|
client.getScreenBuffer().translateToUnicode();
|
||||||
|
terminalPanel.refreshScreen();
|
||||||
|
statusBar.updateStatus();
|
||||||
|
}
|
||||||
|
haus.nightmare.j3270.config.Settings.setCodePage(cpId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeGraphicsMode(GraphicsMode mode) {
|
||||||
|
if (client != null && client.getDataStreamProcessor() != null) {
|
||||||
|
client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode);
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
haus.nightmare.j3270.config.Settings.setGraphicsMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printScreen() {
|
||||||
|
if (client == null) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Connect to a host before printing screen.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrinterJob job = PrinterJob.getPrinterJob();
|
||||||
|
job.setJobName("j3270 Screen Capture");
|
||||||
|
job.setPrintable(terminalPanel);
|
||||||
|
if (job.printDialog()) {
|
||||||
|
try {
|
||||||
|
job.print();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void showFileTransferDialog() {
|
private void showFileTransferDialog() {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
||||||
@@ -187,7 +453,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
monitor.setMillisToDecideToPopup(0);
|
monitor.setMillisToDecideToPopup(0);
|
||||||
monitor.setMillisToPopup(0);
|
monitor.setMillisToPopup(0);
|
||||||
|
|
||||||
// Start a timer to check for manual cancellation
|
|
||||||
Timer cancelCheckTimer = new Timer(500, e -> {
|
Timer cancelCheckTimer = new Timer(500, e -> {
|
||||||
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
||||||
fileTransfer.cancel();
|
fileTransfer.cancel();
|
||||||
@@ -208,7 +473,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
public void onBytesTransferred(long bytes) {
|
public void onBytesTransferred(long bytes) {
|
||||||
if (monitor != null) {
|
if (monitor != null) {
|
||||||
monitor.setNote(bytes + " bytes transferred");
|
monitor.setNote(bytes + " bytes transferred");
|
||||||
// We don't always know max size in IND$FILE, so we just pulse/update note
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,17 +530,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
lastHost = config.getHost();
|
lastHost = config.getHost();
|
||||||
lastPort = config.getPort();
|
lastPort = config.getPort();
|
||||||
|
|
||||||
// Save for auto-connect
|
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||||
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
||||||
|
|
||||||
// Disconnect existing connection
|
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|
||||||
// Wire interactive certificate verifier if none configured
|
|
||||||
if (config.getCertificateVerifier() == null) {
|
if (config.getCertificateVerifier() == null) {
|
||||||
config.setCertificateVerifier((chain, authType, exception) ->
|
config.setCertificateVerifier((chain, authType, exception) ->
|
||||||
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
|
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
|
||||||
@@ -293,7 +554,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||||
|
|
||||||
// Resize window to match model
|
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
@@ -309,7 +569,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}, "Connect-Thread").start();
|
}, "Connect-Thread").start();
|
||||||
|
|
||||||
// Focus the terminal panel
|
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,8 +591,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
int current = terminalPanel.getFontSize();
|
int current = terminalPanel.getFontSize();
|
||||||
int newSize = Math.max(8, Math.min(72, current + delta));
|
int newSize = Math.max(8, Math.min(72, current + delta));
|
||||||
terminalPanel.setFontSize(newSize);
|
terminalPanel.setFontSize(newSize);
|
||||||
// Pack after font change — setFontSize sets the resize guard
|
|
||||||
// to prevent the componentResized from re-triggering autoFitFont
|
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +603,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
statusBar.updateStatus();
|
statusBar.updateStatus();
|
||||||
terminalPanel.repaint();
|
terminalPanel.repaint();
|
||||||
|
|
||||||
// Auto-resize and focus on first full session
|
|
||||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
@@ -367,6 +623,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (deviceName != null) {
|
if (deviceName != null) {
|
||||||
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
||||||
}
|
}
|
||||||
|
statusBar.updateStatus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,10 +634,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (screenUpdatePending.compareAndSet(false, true)) {
|
if (screenUpdatePending.compareAndSet(false, true)) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
screenUpdatePending.set(false);
|
screenUpdatePending.set(false);
|
||||||
// During an active file transfer, let the CUT/DFT handler drive
|
|
||||||
// keyboard state. In x3270, ft_cut_data() runs before WCC
|
|
||||||
// keyboard-restore is applied — the keyboard stays locked for the
|
|
||||||
// entire CUT transfer.
|
|
||||||
if (fileTransfer != null) {
|
if (fileTransfer != null) {
|
||||||
fileTransfer.onScreenUpdated();
|
fileTransfer.onScreenUpdated();
|
||||||
}
|
}
|
||||||
@@ -402,6 +655,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
public void onScreenSizeChanged(int rows, int cols) {
|
public void onScreenSizeChanged(int rows, int cols) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
terminalPanel.guardedPack();
|
terminalPanel.guardedPack();
|
||||||
|
statusBar.updateStatus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,11 +677,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
"Insert — Toggle insert mode\n" +
|
"Insert — Toggle insert mode\n" +
|
||||||
"Escape — Reset\n" +
|
"Escape — Reset\n" +
|
||||||
"PageUp/Down — PF7/PF8\n" +
|
"PageUp/Down — PF7/PF8\n" +
|
||||||
"Alt+C — Clear\n" +
|
"Alt+C / Ctrl+K — Clear\n" +
|
||||||
"Alt+1/2/3 — PA1/PA2/PA3\n" +
|
"Alt+E — Erase Input\n" +
|
||||||
"Cmd+D — Disconnect\n" +
|
"Alt+A — Attention\n" +
|
||||||
"Cmd+Q — Quit\n" +
|
"Alt+S — System Request\n" +
|
||||||
"Cmd+=/-/0 — Font size +/-/reset";
|
"Alt+Q — Cursor Select\n" +
|
||||||
|
"Alt+L — Toggle Light Pen\n" +
|
||||||
|
"Cmd/Ctrl+F — Find on Screen\n" +
|
||||||
|
"Cmd/Ctrl+G / F3— Find Next\n" +
|
||||||
|
"Cmd/Ctrl+T — File Transfer\n" +
|
||||||
|
"Cmd/Ctrl+D — Disconnect\n" +
|
||||||
|
"Cmd/Ctrl+Q — Quit\n" +
|
||||||
|
"Cmd/Ctrl+=/-/0 — Font size +/-/reset";
|
||||||
|
|
||||||
JTextArea area = new JTextArea(text);
|
JTextArea area = new JTextArea(text);
|
||||||
area.setEditable(false);
|
area.setEditable(false);
|
||||||
@@ -435,18 +696,56 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
area.setBackground(new Color(30, 30, 30));
|
area.setBackground(new Color(30, 30, 30));
|
||||||
area.setForeground(new Color(200, 200, 200));
|
area.setForeground(new Color(200, 200, 200));
|
||||||
JScrollPane sp = new JScrollPane(area);
|
JScrollPane sp = new JScrollPane(area);
|
||||||
sp.setPreferredSize(new Dimension(360, 400));
|
sp.setPreferredSize(new Dimension(380, 430));
|
||||||
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
|
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showMnemonicHelp() {
|
||||||
|
String text = "IBM Host On-Demand ECL Mnemonic Tokens:\n\n" +
|
||||||
|
"[enter] — Send Enter key\n" +
|
||||||
|
"[tab] — Tab to next field\n" +
|
||||||
|
"[backtab] — Back-tab to previous field\n" +
|
||||||
|
"[clear] — Clear screen\n" +
|
||||||
|
"[reset] — Reset keyboard lock\n" +
|
||||||
|
"[eraseeof] — Erase to End-of-Field\n" +
|
||||||
|
"[eraseinput] — Erase all unprotected fields\n" +
|
||||||
|
"[newline] — Advance to next line\n" +
|
||||||
|
"[dup] — Insert Duplicate order (0x1C)\n" +
|
||||||
|
"[fm] — Insert Field Mark order (0x1E)\n" +
|
||||||
|
"[attn] — Send Attention signal\n" +
|
||||||
|
"[sysreq] — System Request\n" +
|
||||||
|
"[cursel] — Cursor Select\n" +
|
||||||
|
"[pf1] - [pf24] — PF1 through PF24 keys\n" +
|
||||||
|
"[pa1] - [pa3] — PA1 through PA3 keys\n" +
|
||||||
|
"[up]/[down] — Cursor up / down\n" +
|
||||||
|
"[left]/[right] — Cursor left / right\n" +
|
||||||
|
"[home] — Cursor to home\n\n" +
|
||||||
|
"Example Script:\n" +
|
||||||
|
"LOGON APPLID(TSO)[enter]USER[tab]PASSWORD[enter][pf3]";
|
||||||
|
|
||||||
|
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));
|
||||||
|
JScrollPane sp = new JScrollPane(area);
|
||||||
|
sp.setPreferredSize(new Dimension(440, 430));
|
||||||
|
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
private void showAbout() {
|
private void showAbout() {
|
||||||
JOptionPane.showMessageDialog(this,
|
JOptionPane.showMessageDialog(this,
|
||||||
"j3270 — Java TN3270 Terminal Emulator\n\n" +
|
"j3270 — Java TN3270 Terminal Emulator\n\n" +
|
||||||
"A Java reimplementation of x3270.\n" +
|
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" +
|
||||||
"lib3270j v0.1.0\n\n" +
|
"lib3270j v0.2.0\n\n" +
|
||||||
"Supports: TN3270, TN3270E (RFC 2355)\n" +
|
"Features:\n" +
|
||||||
"Models: IBM 3278/3279 Models 2-5\n" +
|
"• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" +
|
||||||
"Colors, Extended Attributes, Query Replies",
|
"• Models 2-5 & Dynamic Geometry\n" +
|
||||||
|
"• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" +
|
||||||
|
"• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" +
|
||||||
|
"• GDDM/GOCA Vector Graphics & Programmed Symbols\n" +
|
||||||
|
"• 22+ Pluggable EBCDIC Codepages & DBCS Support\n" +
|
||||||
|
"• ECL Presentation Space Search, Inspector & Scripting",
|
||||||
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +756,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
boolean cliTls = false;
|
boolean cliTls = false;
|
||||||
boolean cliNoVerifyCert = false;
|
boolean cliNoVerifyCert = false;
|
||||||
Boolean cliTn3270e = null;
|
Boolean cliTn3270e = null;
|
||||||
haus.nightmare.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
GraphicsMode cliGraphicsMode = null;
|
||||||
String configFile = null;
|
String configFile = null;
|
||||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
@@ -472,11 +771,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
} else if ("--tn3270e".equals(args[i])) {
|
} else if ("--tn3270e".equals(args[i])) {
|
||||||
cliTn3270e = true;
|
cliTn3270e = true;
|
||||||
} else if (args[i].startsWith("--graphics=")) {
|
} else if (args[i].startsWith("--graphics=")) {
|
||||||
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
|
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 (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||||
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
|
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
|
||||||
} else if ("--no-graphics".equals(args[i])) {
|
} else if ("--no-graphics".equals(args[i])) {
|
||||||
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
cliGraphicsMode = GraphicsMode.NONE;
|
||||||
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||||
configFile = args[++i];
|
configFile = args[++i];
|
||||||
} else {
|
} else {
|
||||||
@@ -484,7 +783,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure logging for application packages
|
|
||||||
Level logLevel = debug ? Level.ALL : Level.INFO;
|
Level logLevel = debug ? Level.ALL : Level.INFO;
|
||||||
Logger globalRoot = Logger.getLogger("");
|
Logger globalRoot = Logger.getLogger("");
|
||||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||||
@@ -524,7 +822,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load INI config file if specified
|
|
||||||
if (configFile != null) {
|
if (configFile != null) {
|
||||||
try {
|
try {
|
||||||
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
||||||
@@ -535,43 +832,38 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dark look and feel
|
|
||||||
try {
|
try {
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.fine("Could not set system look and feel");
|
log.fine("Could not set system look and feel");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use native macOS menu bar if applicable
|
|
||||||
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
||||||
System.setProperty("apple.awt.application.name", "j3270");
|
System.setProperty("apple.awt.application.name", "j3270");
|
||||||
|
|
||||||
final boolean finalTls = cliTls;
|
final boolean finalTls = cliTls;
|
||||||
final boolean finalNoVerify = cliNoVerifyCert;
|
final boolean finalNoVerify = cliNoVerifyCert;
|
||||||
final Boolean finalTn3270e = cliTn3270e;
|
final Boolean finalTn3270e = cliTn3270e;
|
||||||
final haus.nightmare.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
J3270App app = new J3270App();
|
J3270App app = new J3270App();
|
||||||
app.setVisible(true);
|
app.setVisible(true);
|
||||||
|
|
||||||
// If host:port given on command line, connect directly
|
|
||||||
if (!remainingArgs.isEmpty()) {
|
if (!remainingArgs.isEmpty()) {
|
||||||
String hostArg = remainingArgs.get(0);
|
String hostArg = remainingArgs.get(0);
|
||||||
int port = finalTls ? 992 : 23;
|
int port = finalTls ? 992 : 23;
|
||||||
if (remainingArgs.size() >= 2) {
|
if (remainingArgs.size() >= 2) {
|
||||||
try {
|
try {
|
||||||
port = Integer.parseInt(remainingArgs.get(1));
|
port = Integer.parseInt(remainingArgs.get(1));
|
||||||
} catch (NumberFormatException ignored) {
|
} catch (NumberFormatException ignored) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TerminalModel model = TerminalModel.IBM_3279_4;
|
TerminalModel model = TerminalModel.IBM_3279_4;
|
||||||
if (remainingArgs.size() >= 3) {
|
if (remainingArgs.size() >= 3) {
|
||||||
try {
|
try {
|
||||||
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
||||||
model = TerminalModel.forModel(modelNum, true);
|
model = TerminalModel.forModel(modelNum, true);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
||||||
if (finalTls) {
|
if (finalTls) {
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
|||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Telnet3270Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a new file transfer with the given configuration.
|
* Start a new file transfer with the given configuration.
|
||||||
* @return null if started successfully, error message otherwise.
|
* @return null if started successfully, error message otherwise.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package haus.nightmare.j3270.ft;
|
package haus.nightmare.j3270.ft;
|
||||||
|
|
||||||
|
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||||
|
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.border.EmptyBorder;
|
import javax.swing.border.EmptyBorder;
|
||||||
@@ -18,6 +20,8 @@ public class FileTransferDialog extends JDialog {
|
|||||||
private JTextField localFileField;
|
private JTextField localFileField;
|
||||||
private JButton browseLocalButton;
|
private JButton browseLocalButton;
|
||||||
private JTextField hostFileField;
|
private JTextField hostFileField;
|
||||||
|
private JButton browseHostButton;
|
||||||
|
private JComboBox<Integer> mtuCombo;
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
private JRadioButton asciiRadio;
|
private JRadioButton asciiRadio;
|
||||||
@@ -40,7 +44,7 @@ public class FileTransferDialog extends JDialog {
|
|||||||
private JButton cancelButton;
|
private JButton cancelButton;
|
||||||
|
|
||||||
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
|
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
|
||||||
super(owner, "File Transfer (IND$FILE)", false); // non-modal to see progress
|
super(owner, "File Transfer (IND$FILE)", false);
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
this.coordinator = coordinator;
|
this.coordinator = coordinator;
|
||||||
|
|
||||||
@@ -76,7 +80,6 @@ public class FileTransferDialog extends JDialog {
|
|||||||
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
|
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
|
||||||
dirPanel.add(sendRadio);
|
dirPanel.add(sendRadio);
|
||||||
|
|
||||||
// Disable append/overwrite when sending
|
|
||||||
sendRadio.addActionListener(e -> updateOptionStates());
|
sendRadio.addActionListener(e -> updateOptionStates());
|
||||||
receiveRadio.addActionListener(e -> updateOptionStates());
|
receiveRadio.addActionListener(e -> updateOptionStates());
|
||||||
|
|
||||||
@@ -94,7 +97,13 @@ public class FileTransferDialog extends JDialog {
|
|||||||
|
|
||||||
// Host File
|
// Host File
|
||||||
hostFileField = new JTextField(20);
|
hostFileField = new JTextField(20);
|
||||||
formPanel.add(createRow("Host File:", hostFileField));
|
browseHostButton = new JButton("Browse Host...");
|
||||||
|
browseHostButton.addActionListener(e -> browseHostDirectory());
|
||||||
|
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
|
||||||
|
hostPanel.setOpaque(false);
|
||||||
|
hostPanel.add(hostFileField, BorderLayout.CENTER);
|
||||||
|
hostPanel.add(browseHostButton, BorderLayout.EAST);
|
||||||
|
formPanel.add(createRow("Host File:", hostPanel));
|
||||||
|
|
||||||
// Mode
|
// Mode
|
||||||
asciiRadio = new JRadioButton("ASCII");
|
asciiRadio = new JRadioButton("ASCII");
|
||||||
@@ -114,6 +123,11 @@ public class FileTransferDialog extends JDialog {
|
|||||||
|
|
||||||
formPanel.add(createRow("Transfer Mode:", modePanel));
|
formPanel.add(createRow("Transfer Mode:", modePanel));
|
||||||
|
|
||||||
|
// MTU Buffer Size
|
||||||
|
mtuCombo = new JComboBox<>(new Integer[]{2048, 4096, 8192, 16384, 32768});
|
||||||
|
mtuCombo.setSelectedItem(FTConstants.DFT_BUF);
|
||||||
|
formPanel.add(createRow("DFT Buffer Size:", mtuCombo));
|
||||||
|
|
||||||
// Options (checkboxes)
|
// Options (checkboxes)
|
||||||
crCheck = new JCheckBox("Add/Remove CR");
|
crCheck = new JCheckBox("Add/Remove CR");
|
||||||
crCheck.setSelected(true);
|
crCheck.setSelected(true);
|
||||||
@@ -198,10 +212,7 @@ public class FileTransferDialog extends JDialog {
|
|||||||
|
|
||||||
setContentPane(mainPanel);
|
setContentPane(mainPanel);
|
||||||
|
|
||||||
// Theme styling for components
|
|
||||||
applyTheme(mainPanel);
|
applyTheme(mainPanel);
|
||||||
|
|
||||||
// Initialize state
|
|
||||||
updateOptionStates();
|
updateOptionStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,14 +247,29 @@ public class FileTransferDialog extends JDialog {
|
|||||||
JFileChooser chooser = new JFileChooser();
|
JFileChooser chooser = new JFileChooser();
|
||||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
|
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
|
||||||
|
|
||||||
// Auto-fill host file if empty
|
|
||||||
if (hostFileField.getText().trim().isEmpty()) {
|
if (hostFileField.getText().trim().isEmpty()) {
|
||||||
hostFileField.setText(chooser.getSelectedFile().getName());
|
hostFileField.setText(chooser.getSelectedFile().getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void browseHostDirectory() {
|
||||||
|
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||||
|
String initialQuery = hostFileField.getText().trim();
|
||||||
|
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
|
||||||
|
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||||
|
hostFileField.setText(dialog.getSelectedHostFile());
|
||||||
|
if (localFileField.getText().trim().isEmpty()) {
|
||||||
|
String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", "");
|
||||||
|
int lastDot = cleanName.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1);
|
||||||
|
localFileField.setText(cleanName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void startTransfer() {
|
private void startTransfer() {
|
||||||
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
|
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
|
||||||
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
|
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
|
||||||
@@ -262,6 +288,11 @@ public class FileTransferDialog extends JDialog {
|
|||||||
config.setAppend(appendCheck.isSelected());
|
config.setAppend(appendCheck.isSelected());
|
||||||
config.setOverwrite(overwriteCheck.isSelected());
|
config.setOverwrite(overwriteCheck.isSelected());
|
||||||
|
|
||||||
|
Integer mtu = (Integer) mtuCombo.getSelectedItem();
|
||||||
|
if (mtu != null) {
|
||||||
|
config.setDftBufferSize(mtu);
|
||||||
|
}
|
||||||
|
|
||||||
config.setRecfm(recfmField.getText().trim());
|
config.setRecfm(recfmField.getText().trim());
|
||||||
config.setLrecl(lreclField.getText().trim());
|
config.setLrecl(lreclField.getText().trim());
|
||||||
config.setBlksize(blksizeField.getText().trim());
|
config.setBlksize(blksizeField.getText().trim());
|
||||||
@@ -272,7 +303,6 @@ public class FileTransferDialog extends JDialog {
|
|||||||
if (error != null) {
|
if (error != null) {
|
||||||
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||||
} else {
|
} else {
|
||||||
// Success, close dialog (progress will be shown separately)
|
|
||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
buttonPanel.add(connectBtn);
|
buttonPanel.add(connectBtn);
|
||||||
|
|
||||||
gbc.gridx = 0;
|
gbc.gridx = 0;
|
||||||
gbc.gridy = 8;
|
gbc.gridy = 9;
|
||||||
gbc.gridwidth = 2;
|
gbc.gridwidth = 2;
|
||||||
mainPanel.add(buttonPanel, gbc);
|
mainPanel.add(buttonPanel, gbc);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||||
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.border.LineBorder;
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer;
|
||||||
|
import javax.swing.table.DefaultTableModel;
|
||||||
|
import javax.swing.table.JTableHeader;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live Field Inspector dialog displaying all fields in the 3270 Presentation Space (ECLFieldList).
|
||||||
|
*/
|
||||||
|
public class FieldInspectorDialog extends JDialog implements ScreenUpdateListener {
|
||||||
|
|
||||||
|
private final Telnet3270Client client;
|
||||||
|
private final TerminalPanel terminalPanel;
|
||||||
|
|
||||||
|
private DefaultTableModel tableModel;
|
||||||
|
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;
|
||||||
|
this.terminalPanel = terminalPanel;
|
||||||
|
|
||||||
|
buildUI();
|
||||||
|
setSize(850, 420);
|
||||||
|
setLocationRelativeTo(parent);
|
||||||
|
|
||||||
|
if (client != null) {
|
||||||
|
client.addScreenUpdateListener(this);
|
||||||
|
refreshFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent e) {
|
||||||
|
if (client != null) {
|
||||||
|
// Screen update listener removed on close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
refreshBtn.addActionListener(e -> refreshFields());
|
||||||
|
topPanel.add(refreshBtn, BorderLayout.EAST);
|
||||||
|
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
// Table
|
||||||
|
String[] columns = {
|
||||||
|
"#", "Pos", "Start", "End", "Len", "Prot", "MDT", "Num", "Hi-Int", "Hidden", "Pen-Sel", "Content Text"
|
||||||
|
};
|
||||||
|
|
||||||
|
tableModel = new DefaultTableModel(columns, 0) {
|
||||||
|
@Override
|
||||||
|
public boolean isCellEditable(int row, int column) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
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
|
||||||
|
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
|
||||||
|
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
|
||||||
|
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center align boolean columns
|
||||||
|
DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
|
||||||
|
centerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
|
||||||
|
for (int i = 5; i <= 10; i++) {
|
||||||
|
table.getColumnModel().getColumn(i).setCellRenderer(centerRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.getSelectionModel().addListSelectionListener(e -> {
|
||||||
|
if (!e.getValueIsAdjusting() && table.getSelectedRow() >= 0) {
|
||||||
|
selectFieldOnTerminal(table.getSelectedRow());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(table);
|
||||||
|
scrollPane.setBorder(new LineBorder(DARK_BORDER));
|
||||||
|
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
|
||||||
|
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Bottom panel
|
||||||
|
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||||
|
bottomPanel.setOpaque(false);
|
||||||
|
|
||||||
|
JButton jumpBtn = new JButton("Jump to Selected Field");
|
||||||
|
jumpBtn.setBackground(new Color(50, 100, 160));
|
||||||
|
jumpBtn.setForeground(Color.WHITE);
|
||||||
|
jumpBtn.addActionListener(e -> {
|
||||||
|
int row = table.getSelectedRow();
|
||||||
|
if (row >= 0) {
|
||||||
|
selectFieldOnTerminal(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JButton closeBtn = new JButton("Close");
|
||||||
|
closeBtn.setBackground(new Color(55, 55, 55));
|
||||||
|
closeBtn.setForeground(DARK_FG);
|
||||||
|
closeBtn.addActionListener(e -> dispose());
|
||||||
|
|
||||||
|
bottomPanel.add(jumpBtn);
|
||||||
|
bottomPanel.add(closeBtn);
|
||||||
|
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
setContentPane(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void refreshFields() {
|
||||||
|
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||||
|
tableModel.setRowCount(0);
|
||||||
|
countLabel.setText("Not connected or screen unformatted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ECLFieldList fieldList = client.getFieldList();
|
||||||
|
List<ECLField> fields = fieldList.getFields();
|
||||||
|
|
||||||
|
int selectedRow = table.getSelectedRow();
|
||||||
|
tableModel.setRowCount(0);
|
||||||
|
|
||||||
|
int cols = client.getScreenBuffer().getDisplayCols();
|
||||||
|
for (int i = 0; i < fields.size(); i++) {
|
||||||
|
ECLField f = fields.get(i);
|
||||||
|
int startRow = f.getStartRow() + 1;
|
||||||
|
int startCol = f.getStartCol() + 1;
|
||||||
|
int endRow = f.getEndRow() + 1;
|
||||||
|
int endCol = f.getEndCol() + 1;
|
||||||
|
|
||||||
|
String text = f.getText();
|
||||||
|
if (f.isHidden()) {
|
||||||
|
text = "••••••••";
|
||||||
|
}
|
||||||
|
|
||||||
|
tableModel.addRow(new Object[]{
|
||||||
|
(i + 1),
|
||||||
|
f.getStart(),
|
||||||
|
String.format("%02d/%02d", startRow, startCol),
|
||||||
|
String.format("%02d/%02d", endRow, endCol),
|
||||||
|
f.getLength(),
|
||||||
|
f.isProtected() ? "YES" : "-",
|
||||||
|
f.isModified() ? "YES" : "-",
|
||||||
|
f.isNumeric() ? "YES" : "-",
|
||||||
|
f.isHighIntensity() ? "YES" : "-",
|
||||||
|
f.isHidden() ? "YES" : "-",
|
||||||
|
f.isPenSelectable() ? "YES" : "-",
|
||||||
|
text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
countLabel.setText(fields.size() + " fields detected on screen (" + client.getScreenBuffer().getDisplayRows() + "x" + cols + ")");
|
||||||
|
if (selectedRow >= 0 && selectedRow < tableModel.getRowCount()) {
|
||||||
|
table.setRowSelectionInterval(selectedRow, selectedRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectFieldOnTerminal(int rowIndex) {
|
||||||
|
if (client == null || rowIndex < 0) return;
|
||||||
|
List<ECLField> fields = client.getFieldList().getFields();
|
||||||
|
if (rowIndex < fields.size()) {
|
||||||
|
ECLField f = fields.get(rowIndex);
|
||||||
|
client.getScreenBuffer().setCursorAddress(f.getDataStart());
|
||||||
|
if (terminalPanel != null) {
|
||||||
|
terminalPanel.setSelectionRange(f.getDataStart(), f.getEnd());
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== ScreenUpdateListener ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScreenUpdated() {
|
||||||
|
SwingUtilities.invokeLater(this::refreshFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScreenSizeChanged(int rows, int cols) {
|
||||||
|
SwingUtilities.invokeLater(this::refreshFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSoundAlarm() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.KeyAdapter;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find dialog for searching text in the 3270 Presentation Space using ECLPS.
|
||||||
|
*/
|
||||||
|
public class FindDialog extends JDialog {
|
||||||
|
|
||||||
|
private final Telnet3270Client client;
|
||||||
|
private final TerminalPanel terminalPanel;
|
||||||
|
|
||||||
|
private JTextField searchField;
|
||||||
|
private JCheckBox matchCaseCheck;
|
||||||
|
private JRadioButton forwardRadio;
|
||||||
|
private JRadioButton backwardRadio;
|
||||||
|
private JLabel statusLabel;
|
||||||
|
|
||||||
|
private static String lastSearchText = "";
|
||||||
|
private static boolean lastMatchCase = false;
|
||||||
|
private static boolean lastForward = true;
|
||||||
|
|
||||||
|
public FindDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||||
|
super(parent, "Find on Screen", false);
|
||||||
|
this.client = client;
|
||||||
|
this.terminalPanel = terminalPanel;
|
||||||
|
|
||||||
|
buildUI();
|
||||||
|
pack();
|
||||||
|
setLocationRelativeTo(parent);
|
||||||
|
setResizable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
formPanel.setOpaque(false);
|
||||||
|
GridBagConstraints gbc = new GridBagConstraints();
|
||||||
|
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);
|
||||||
|
|
||||||
|
gbc.gridx = 1;
|
||||||
|
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)));
|
||||||
|
formPanel.add(searchField, gbc);
|
||||||
|
|
||||||
|
// Options
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.gridy = 1;
|
||||||
|
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
|
||||||
|
matchCaseCheck.setForeground(fg);
|
||||||
|
matchCaseCheck.setOpaque(false);
|
||||||
|
matchCaseCheck.setFont(labelFont);
|
||||||
|
matchCaseCheck.setFocusPainted(false);
|
||||||
|
formPanel.add(matchCaseCheck, gbc);
|
||||||
|
|
||||||
|
// Direction
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.gridy = 2;
|
||||||
|
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);
|
||||||
|
|
||||||
|
ButtonGroup bg = new ButtonGroup();
|
||||||
|
bg.add(forwardRadio);
|
||||||
|
bg.add(backwardRadio);
|
||||||
|
dirPanel.add(dirLabel);
|
||||||
|
dirPanel.add(forwardRadio);
|
||||||
|
dirPanel.add(Box.createHorizontalStrut(10));
|
||||||
|
dirPanel.add(backwardRadio);
|
||||||
|
formPanel.add(dirPanel, gbc);
|
||||||
|
|
||||||
|
// Status
|
||||||
|
gbc.gridx = 0;
|
||||||
|
gbc.gridy = 3;
|
||||||
|
gbc.gridwidth = 2;
|
||||||
|
statusLabel = new JLabel(" ");
|
||||||
|
statusLabel.setForeground(new Color(255, 120, 120));
|
||||||
|
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||||
|
formPanel.add(statusLabel, gbc);
|
||||||
|
|
||||||
|
mainPanel.add(formPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||||
|
buttonPanel.setOpaque(false);
|
||||||
|
|
||||||
|
JButton findNextBtn = new JButton("Find Next");
|
||||||
|
findNextBtn.setBackground(new Color(60, 63, 65));
|
||||||
|
findNextBtn.setForeground(fg);
|
||||||
|
findNextBtn.setFont(labelFont);
|
||||||
|
findNextBtn.addActionListener(e -> findNext());
|
||||||
|
|
||||||
|
JButton closeBtn = new JButton("Close");
|
||||||
|
closeBtn.setBackground(new Color(60, 63, 65));
|
||||||
|
closeBtn.setForeground(fg);
|
||||||
|
closeBtn.setFont(labelFont);
|
||||||
|
closeBtn.addActionListener(e -> dispose());
|
||||||
|
|
||||||
|
buttonPanel.add(findNextBtn);
|
||||||
|
buttonPanel.add(closeBtn);
|
||||||
|
|
||||||
|
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
setContentPane(mainPanel);
|
||||||
|
getRootPane().setDefaultButton(findNextBtn);
|
||||||
|
|
||||||
|
searchField.addKeyListener(new KeyAdapter() {
|
||||||
|
@Override
|
||||||
|
public void keyPressed(KeyEvent e) {
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||||
|
findNext();
|
||||||
|
} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void findNext() {
|
||||||
|
find(forwardRadio.isSelected());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void findPrevious() {
|
||||||
|
find(!forwardRadio.isSelected());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void find(boolean forward) {
|
||||||
|
String target = searchField.getText();
|
||||||
|
if (target == null || target.isEmpty()) {
|
||||||
|
statusLabel.setText("Please enter search text.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||||
|
statusLabel.setText("Terminal is not connected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSearchText = target;
|
||||||
|
lastMatchCase = matchCaseCheck.isSelected();
|
||||||
|
lastForward = forwardRadio.isSelected();
|
||||||
|
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int curAddr = sb.getCursorAddress();
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
int curRow = curAddr / cols;
|
||||||
|
int curCol = curAddr % cols;
|
||||||
|
|
||||||
|
// Offset search start position past current character so repeat search moves to next occurrence
|
||||||
|
int startRow = curRow;
|
||||||
|
int startCol = curCol;
|
||||||
|
if (forward) {
|
||||||
|
startCol = (curCol + 1) % cols;
|
||||||
|
if (startCol == 0) startRow = (curRow + 1) % sb.getDisplayRows();
|
||||||
|
} else {
|
||||||
|
startCol = (curCol - 1 + cols) % cols;
|
||||||
|
if (startCol == cols - 1) startRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
int dir = forward ? ECLConstants.SEARCH_FORWARD : ECLConstants.SEARCH_BACKWARD;
|
||||||
|
int foundPos = client.getPS().searchString(target, startRow, startCol, dir, !matchCaseCheck.isSelected());
|
||||||
|
|
||||||
|
if (foundPos >= 0) {
|
||||||
|
statusLabel.setText("Found at position " + (foundPos + 1) + " (" + (foundPos / cols + 1) + "/" + (foundPos % cols + 1) + ")");
|
||||||
|
statusLabel.setForeground(new Color(100, 255, 100));
|
||||||
|
sb.setCursorAddress(foundPos);
|
||||||
|
if (terminalPanel != null) {
|
||||||
|
terminalPanel.setSearchHighlight(foundPos, target.length());
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusLabel.setText("Text not found: \"" + target + "\"");
|
||||||
|
statusLabel.setForeground(new Color(255, 120, 120));
|
||||||
|
if (terminalPanel != null) {
|
||||||
|
terminalPanel.clearSearchHighlight();
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getLastSearchText() {
|
||||||
|
return lastSearchText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getLastMatchCase() {
|
||||||
|
return lastMatchCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getLastForward() {
|
||||||
|
return lastForward;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.border.LineBorder;
|
||||||
|
import javax.swing.table.DefaultTableModel;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
|
||||||
|
*/
|
||||||
|
public class HostDirectoryDialog extends JDialog {
|
||||||
|
|
||||||
|
private final Telnet3270Client client;
|
||||||
|
private final FTConfig.HostType initialHostType;
|
||||||
|
|
||||||
|
private JComboBox<FTConfig.HostType> hostTypeCombo;
|
||||||
|
private JTextField queryField;
|
||||||
|
private JTable table;
|
||||||
|
private DefaultTableModel tableModel;
|
||||||
|
private JLabel statusLabel;
|
||||||
|
|
||||||
|
private String selectedHostFile = null;
|
||||||
|
private boolean confirmed = false;
|
||||||
|
|
||||||
|
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
|
||||||
|
super(parent, "Host Catalog & Directory Browser", true);
|
||||||
|
this.client = client;
|
||||||
|
this.initialHostType = hostType;
|
||||||
|
|
||||||
|
buildUI(initialQuery);
|
||||||
|
setSize(700, 420);
|
||||||
|
setLocationRelativeTo(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
JPanel topPanel = new JPanel(new GridBagLayout());
|
||||||
|
topPanel.setOpaque(false);
|
||||||
|
GridBagConstraints gbc = new GridBagConstraints();
|
||||||
|
gbc.insets = new Insets(3, 4, 3, 4);
|
||||||
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
|
||||||
|
JLabel envLabel = new JLabel("System:");
|
||||||
|
envLabel.setForeground(fg);
|
||||||
|
envLabel.setFont(font);
|
||||||
|
topPanel.add(envLabel, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 1;
|
||||||
|
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);
|
||||||
|
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)));
|
||||||
|
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);
|
||||||
|
parseBtn.setFont(font);
|
||||||
|
parseBtn.addActionListener(e -> runQuery());
|
||||||
|
topPanel.add(parseBtn, gbc);
|
||||||
|
|
||||||
|
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
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() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.getClickCount() == 2 && table.getSelectedRow() >= 0) {
|
||||||
|
onConfirmSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(table);
|
||||||
|
scrollPane.setBorder(new LineBorder(new Color(65, 65, 65)));
|
||||||
|
scrollPane.getViewport().setBackground(new Color(45, 45, 45));
|
||||||
|
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Bottom
|
||||||
|
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||||
|
bottomPanel.setOpaque(false);
|
||||||
|
|
||||||
|
statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
|
||||||
|
statusLabel.setForeground(new Color(170, 170, 170));
|
||||||
|
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||||
|
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||||
|
|
||||||
|
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||||
|
btnPanel.setOpaque(false);
|
||||||
|
|
||||||
|
JButton pasteScreenBtn = new JButton("Parse Current Screen");
|
||||||
|
pasteScreenBtn.setBackground(new Color(50, 50, 50));
|
||||||
|
pasteScreenBtn.setForeground(fg);
|
||||||
|
pasteScreenBtn.setFont(font);
|
||||||
|
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
|
||||||
|
|
||||||
|
JButton selectBtn = new JButton("Select Dataset");
|
||||||
|
selectBtn.setBackground(new Color(50, 120, 50));
|
||||||
|
selectBtn.setForeground(Color.WHITE);
|
||||||
|
selectBtn.setFont(font);
|
||||||
|
selectBtn.addActionListener(e -> onConfirmSelection());
|
||||||
|
|
||||||
|
JButton cancelBtn = new JButton("Cancel");
|
||||||
|
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||||
|
cancelBtn.setForeground(fg);
|
||||||
|
cancelBtn.setFont(font);
|
||||||
|
cancelBtn.addActionListener(e -> {
|
||||||
|
confirmed = false;
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnPanel.add(pasteScreenBtn);
|
||||||
|
btnPanel.add(Box.createHorizontalStrut(10));
|
||||||
|
btnPanel.add(selectBtn);
|
||||||
|
btnPanel.add(cancelBtn);
|
||||||
|
|
||||||
|
bottomPanel.add(btnPanel, BorderLayout.EAST);
|
||||||
|
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
setContentPane(mainPanel);
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
setupTableColumns();
|
||||||
|
if (initialQuery != null && !initialQuery.isEmpty()) {
|
||||||
|
runQuery();
|
||||||
|
} else {
|
||||||
|
parseCurrentScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupTableColumns() {
|
||||||
|
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||||
|
tableModel.setRowCount(0);
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
tableModel.setColumnIdentifiers(new Object[]{
|
||||||
|
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tableModel.setColumnIdentifiers(new Object[]{
|
||||||
|
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runQuery() {
|
||||||
|
setupTableColumns();
|
||||||
|
currentEntries.clear();
|
||||||
|
String query = queryField.getText().trim();
|
||||||
|
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||||
|
|
||||||
|
if (client != null && client.getXfer() != null) {
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
|
||||||
|
currentEntries.addAll(cmsEntries);
|
||||||
|
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||||
|
tableModel.addRow(new Object[]{
|
||||||
|
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||||
|
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
|
||||||
|
currentEntries.addAll(tsoEntries);
|
||||||
|
for (TSODirectoryEntry entry : tsoEntries) {
|
||||||
|
tableModel.addRow(new Object[]{
|
||||||
|
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||||
|
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel.setText(currentEntries.size() + " entries found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseCurrentScreen() {
|
||||||
|
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||||
|
statusLabel.setText("No active session to parse.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTableColumns();
|
||||||
|
currentEntries.clear();
|
||||||
|
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||||
|
String screenText = client.getPS().getString(0, client.getPS().getSize());
|
||||||
|
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText);
|
||||||
|
currentEntries.addAll(cmsEntries);
|
||||||
|
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||||
|
tableModel.addRow(new Object[]{
|
||||||
|
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||||
|
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText);
|
||||||
|
currentEntries.addAll(tsoEntries);
|
||||||
|
for (TSODirectoryEntry entry : tsoEntries) {
|
||||||
|
tableModel.addRow(new Object[]{
|
||||||
|
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||||
|
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onConfirmSelection() {
|
||||||
|
int row = table.getSelectedRow();
|
||||||
|
if (row < 0 || row >= currentEntries.size()) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Please select an entry from the table.",
|
||||||
|
"No Selection", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HostDirectoryEntry entry = currentEntries.get(row);
|
||||||
|
selectedHostFile = entry.getName();
|
||||||
|
confirmed = true;
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfirmed() {
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSelectedHostFile() {
|
||||||
|
return selectedHostFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.printer.*;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.border.LineBorder;
|
||||||
|
import javax.swing.border.TitledBorder;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.print.PrinterJob;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IBM 3287 / 3286 TN3270E Printer Session Manager and Spool Viewer.
|
||||||
|
* Integrates directly with lib3270j.printer.Telnet3270EP and PD3270.
|
||||||
|
*/
|
||||||
|
public class PrinterSessionDialog extends JDialog implements PrintSessionListener {
|
||||||
|
|
||||||
|
private final Frame parent;
|
||||||
|
private PrinterConfig config;
|
||||||
|
private Telnet3270EP printerSession;
|
||||||
|
|
||||||
|
// Configuration Fields
|
||||||
|
private JTextField hostField;
|
||||||
|
private JTextField portField;
|
||||||
|
private JTextField printerLuField;
|
||||||
|
private JTextField displayLuField;
|
||||||
|
private JComboBox<String> codePageCombo;
|
||||||
|
private JCheckBox tlsCheck;
|
||||||
|
private JCheckBox verifyCertCheck;
|
||||||
|
private JComboBox<PrinterConfig.DestinationType> destinationCombo;
|
||||||
|
private JTextField targetField;
|
||||||
|
|
||||||
|
// Status Fields
|
||||||
|
private JLabel statusLabel;
|
||||||
|
private JLabel sessionTypeLabel;
|
||||||
|
private JLabel pagesLabel;
|
||||||
|
private JLabel bytesLabel;
|
||||||
|
private JButton connectBtn;
|
||||||
|
private JButton disconnectBtn;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
config = new PrinterConfig(defaultHost, defaultPort);
|
||||||
|
config.setUseTls(defaultTls);
|
||||||
|
config.setDestinationType(PrinterConfig.DestinationType.MEMORY);
|
||||||
|
|
||||||
|
buildUI();
|
||||||
|
setSize(780, 560);
|
||||||
|
setLocationRelativeTo(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
GridBagConstraints gbc = new GridBagConstraints();
|
||||||
|
gbc.insets = new Insets(3, 4, 3, 4);
|
||||||
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
|
||||||
|
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||||
|
|
||||||
|
// Host / Port
|
||||||
|
gbc.gridx = 0; gbc.gridy = 0;
|
||||||
|
JLabel hLbl = new JLabel("Host:");
|
||||||
|
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont);
|
||||||
|
topPanel.add(hLbl, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||||
|
hostField = createDarkField(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);
|
||||||
|
topPanel.add(pLbl, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||||
|
portField = createDarkField(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);
|
||||||
|
topPanel.add(pluLbl, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||||
|
printerLuField = createDarkField("", 10);
|
||||||
|
topPanel.add(printerLuField, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 2; gbc.weightx = 0;
|
||||||
|
JLabel assocLbl = new JLabel("Assoc LU:");
|
||||||
|
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont);
|
||||||
|
topPanel.add(assocLbl, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||||
|
displayLuField = createDarkField("", 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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
|
||||||
|
verifyCertCheck.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false);
|
||||||
|
tlsPanel.add(tlsCheck);
|
||||||
|
tlsPanel.add(verifyCertCheck);
|
||||||
|
topPanel.add(tlsPanel, gbc);
|
||||||
|
|
||||||
|
// Destination Type & Target
|
||||||
|
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
|
||||||
|
JLabel destLbl = new JLabel("Destination:");
|
||||||
|
destLbl.setForeground(DARK_FG); 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);
|
||||||
|
topPanel.add(destinationCombo, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 2;
|
||||||
|
JLabel tgtLbl = new JLabel("Target Path:");
|
||||||
|
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont);
|
||||||
|
topPanel.add(tgtLbl, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 3;
|
||||||
|
targetField = createDarkField("printer_output.txt", 12);
|
||||||
|
topPanel.add(targetField, gbc);
|
||||||
|
|
||||||
|
// Connect / Disconnect Buttons
|
||||||
|
gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 4;
|
||||||
|
JPanel connBtnPan = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||||
|
connBtnPan.setOpaque(false);
|
||||||
|
|
||||||
|
connectBtn = new JButton("Start Printer Session");
|
||||||
|
connectBtn.setBackground(new Color(50, 120, 50));
|
||||||
|
connectBtn.setForeground(Color.WHITE);
|
||||||
|
connectBtn.addActionListener(e -> startPrinterSession());
|
||||||
|
|
||||||
|
disconnectBtn = new JButton("Stop Session");
|
||||||
|
disconnectBtn.setBackground(new Color(120, 50, 50));
|
||||||
|
disconnectBtn.setForeground(Color.WHITE);
|
||||||
|
disconnectBtn.setEnabled(false);
|
||||||
|
disconnectBtn.addActionListener(e -> stopPrinterSession());
|
||||||
|
|
||||||
|
connBtnPan.add(connectBtn);
|
||||||
|
connBtnPan.add(disconnectBtn);
|
||||||
|
topPanel.add(connBtnPan, gbc);
|
||||||
|
|
||||||
|
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Status Header
|
||||||
|
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
|
||||||
|
statusHeader.setOpaque(false);
|
||||||
|
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
|
||||||
|
|
||||||
|
statusLabel = new JLabel("Status: Disconnected");
|
||||||
|
statusLabel.setForeground(new Color(180, 180, 180));
|
||||||
|
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);
|
||||||
|
statusHeader.add(sessionTypeLabel);
|
||||||
|
statusHeader.add(pagesLabel);
|
||||||
|
statusHeader.add(bytesLabel);
|
||||||
|
centerPanel.add(statusHeader, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
JScrollPane spoolScroll = new JScrollPane(spoolArea);
|
||||||
|
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60)));
|
||||||
|
centerPanel.add(spoolScroll, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
mainPanel.add(centerPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Bottom: Spool controls
|
||||||
|
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||||
|
bottomPanel.setOpaque(false);
|
||||||
|
|
||||||
|
JButton clearSpoolBtn = new JButton("Clear Spool");
|
||||||
|
clearSpoolBtn.setBackground(new Color(50, 50, 50));
|
||||||
|
clearSpoolBtn.setForeground(DARK_FG);
|
||||||
|
clearSpoolBtn.addActionListener(e -> {
|
||||||
|
spoolArea.setText("");
|
||||||
|
if (printerSession != null && printerSession.getPD() != null) {
|
||||||
|
printerSession.getPD().resetCapture();
|
||||||
|
}
|
||||||
|
pagesLabel.setText("Pages: 0");
|
||||||
|
bytesLabel.setText("Bytes: 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
JButton saveSpoolBtn = new JButton("Save Spool As...");
|
||||||
|
saveSpoolBtn.setBackground(new Color(50, 50, 50));
|
||||||
|
saveSpoolBtn.setForeground(DARK_FG);
|
||||||
|
saveSpoolBtn.addActionListener(e -> saveSpool());
|
||||||
|
|
||||||
|
JButton printSpoolBtn = new JButton("Print Spool...");
|
||||||
|
printSpoolBtn.setBackground(new Color(50, 100, 160));
|
||||||
|
printSpoolBtn.setForeground(Color.WHITE);
|
||||||
|
printSpoolBtn.addActionListener(e -> printSpool());
|
||||||
|
|
||||||
|
JButton closeBtn = new JButton("Close");
|
||||||
|
closeBtn.setBackground(new Color(60, 60, 60));
|
||||||
|
closeBtn.setForeground(DARK_FG);
|
||||||
|
closeBtn.addActionListener(e -> dispose());
|
||||||
|
|
||||||
|
bottomPanel.add(clearSpoolBtn);
|
||||||
|
bottomPanel.add(saveSpoolBtn);
|
||||||
|
bottomPanel.add(printSpoolBtn);
|
||||||
|
bottomPanel.add(Box.createHorizontalStrut(10));
|
||||||
|
bottomPanel.add(closeBtn);
|
||||||
|
|
||||||
|
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
setContentPane(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JTextField createDarkField(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)));
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPrinterSession() {
|
||||||
|
String host = hostField.getText().trim();
|
||||||
|
if (host.isEmpty()) {
|
||||||
|
hostField.requestFocus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int port = 23;
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(portField.getText().trim());
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
|
||||||
|
config.setHost(host);
|
||||||
|
config.setPort(port);
|
||||||
|
config.setUseTls(tlsCheck.isSelected());
|
||||||
|
config.setTlsVerifyCert(verifyCertCheck.isSelected());
|
||||||
|
config.setCodePage((String) codePageCombo.getSelectedItem());
|
||||||
|
|
||||||
|
String plu = printerLuField.getText().trim();
|
||||||
|
config.setPrinterLuName(plu.isEmpty() ? null : plu);
|
||||||
|
|
||||||
|
String dlu = displayLuField.getText().trim();
|
||||||
|
config.setAssociatedDisplayLuName(dlu.isEmpty() ? null : dlu);
|
||||||
|
|
||||||
|
config.setDestinationType((PrinterConfig.DestinationType) destinationCombo.getSelectedItem());
|
||||||
|
config.setDestinationTarget(targetField.getText().trim());
|
||||||
|
|
||||||
|
if (printerSession != null) {
|
||||||
|
printerSession.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
printerSession = new Telnet3270EP(config);
|
||||||
|
printerSession.addPrintListener(this);
|
||||||
|
|
||||||
|
connectBtn.setEnabled(false);
|
||||||
|
disconnectBtn.setEnabled(true);
|
||||||
|
statusLabel.setText("Status: Connecting...");
|
||||||
|
statusLabel.setForeground(new Color(255, 200, 80));
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
boolean success = printerSession.open();
|
||||||
|
if (!success) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
connectBtn.setEnabled(true);
|
||||||
|
disconnectBtn.setEnabled(false);
|
||||||
|
statusLabel.setText("Status: Connection Failed");
|
||||||
|
statusLabel.setForeground(new Color(255, 80, 80));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, "PrinterSession-Thread").start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopPrinterSession() {
|
||||||
|
if (printerSession != null) {
|
||||||
|
printerSession.close();
|
||||||
|
printerSession = null;
|
||||||
|
}
|
||||||
|
connectBtn.setEnabled(true);
|
||||||
|
disconnectBtn.setEnabled(false);
|
||||||
|
statusLabel.setText("Status: Disconnected");
|
||||||
|
statusLabel.setForeground(new Color(180, 180, 180));
|
||||||
|
sessionTypeLabel.setText("Session: -");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveSpool() {
|
||||||
|
JFileChooser fc = new JFileChooser();
|
||||||
|
fc.setDialogTitle("Save Printer Spool Output");
|
||||||
|
fc.setSelectedFile(new File("spool_output.txt"));
|
||||||
|
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(fc.getSelectedFile())) {
|
||||||
|
fos.write(spoolArea.getText().getBytes(StandardCharsets.UTF_8));
|
||||||
|
JOptionPane.showMessageDialog(this, "Spool saved successfully to " + fc.getSelectedFile().getName(),
|
||||||
|
"Saved", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Save error: " + e.getMessage(),
|
||||||
|
"Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printSpool() {
|
||||||
|
String text = spoolArea.getText();
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Spool buffer is empty.", "Nothing to Print", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrinterJob job = PrinterJob.getPrinterJob();
|
||||||
|
job.setJobName("j3270 3287 Print Spool");
|
||||||
|
if (job.printDialog()) {
|
||||||
|
try {
|
||||||
|
spoolArea.print();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== PrintSessionListener ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrintJobStarted(PrintSessionEvent event) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
statusLabel.setText("Status: Printing...");
|
||||||
|
statusLabel.setForeground(new Color(50, 205, 50));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrintJobData(PrintSessionEvent event) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
if (printerSession != null && printerSession.getPD() != null) {
|
||||||
|
spoolArea.setText(printerSession.getPD().getCapturedText());
|
||||||
|
spoolArea.setCaretPosition(spoolArea.getDocument().getLength());
|
||||||
|
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
|
||||||
|
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrintJobPageComplete(PrintSessionEvent event) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
if (printerSession != null && printerSession.getPD() != null) {
|
||||||
|
spoolArea.setText(printerSession.getPD().getCapturedText());
|
||||||
|
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
|
||||||
|
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrintJobComplete(PrintSessionEvent event) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
if (printerSession != null && printerSession.getPD() != null) {
|
||||||
|
spoolArea.setText(printerSession.getPD().getCapturedText());
|
||||||
|
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
|
||||||
|
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
|
||||||
|
}
|
||||||
|
statusLabel.setText("Status: In Session (Idle)");
|
||||||
|
statusLabel.setForeground(new Color(50, 205, 50));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrinterStatusChanged(PrintSessionEvent event) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
int code = event.getStatusCode();
|
||||||
|
if (code == PrinterConstants.STATUS_CONNECTED || code == PrinterConstants.STATUS_PRINTER_READY || code == PrinterConstants.STATUS_PRINTING || code == PrinterConstants.STATUS_JOB_COMPLETE || code == PrinterConstants.STATUS_PAGE_COMPLETE) {
|
||||||
|
String lu = printerSession != null ? printerSession.getAssignedLuName() : null;
|
||||||
|
statusLabel.setText("Status: Connected" + (lu != null ? " (" + lu + ")" : ""));
|
||||||
|
statusLabel.setForeground(new Color(50, 205, 50));
|
||||||
|
connectBtn.setEnabled(false);
|
||||||
|
disconnectBtn.setEnabled(true);
|
||||||
|
|
||||||
|
if (printerSession != null) {
|
||||||
|
short luType = printerSession.getActiveLuType();
|
||||||
|
if (luType == PrinterConstants.LU_TYPE_1_SCS) {
|
||||||
|
sessionTypeLabel.setText("Session: LU1 (SCS)");
|
||||||
|
} else if (luType == PrinterConstants.LU_TYPE_3_DS) {
|
||||||
|
sessionTypeLabel.setText("Session: LU3 (3270 DS)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (code == PrinterConstants.STATUS_DISCONNECTED) {
|
||||||
|
statusLabel.setText("Status: Disconnected");
|
||||||
|
statusLabel.setForeground(new Color(180, 180, 180));
|
||||||
|
connectBtn.setEnabled(true);
|
||||||
|
disconnectBtn.setEnabled(false);
|
||||||
|
sessionTypeLabel.setText("Session: -");
|
||||||
|
} else {
|
||||||
|
statusLabel.setText("Status: " + event.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrinterError(PrintSessionEvent event) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
statusLabel.setText("Status: Error (" + event.getMessage() + ")");
|
||||||
|
statusLabel.setForeground(new Color(255, 80, 80));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
stopPrinterSession();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for exporting the 3270 Presentation Space to Plain Text, HTML, or PNG image.
|
||||||
|
*/
|
||||||
|
public class ScreenExporter {
|
||||||
|
|
||||||
|
public static void showExportDialog(Frame parent, Telnet3270Client client, TerminalPanel panel) {
|
||||||
|
if (client == null) {
|
||||||
|
JOptionPane.showMessageDialog(parent, "Connect to a host before exporting the screen.",
|
||||||
|
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JFileChooser fc = new JFileChooser();
|
||||||
|
fc.setDialogTitle("Save Screen As...");
|
||||||
|
fc.addChoosableFileFilter(new FileNameExtensionFilter("PNG Image (*.png)", "png"));
|
||||||
|
fc.addChoosableFileFilter(new FileNameExtensionFilter("HTML Document (*.html)", "html", "htm"));
|
||||||
|
FileNameExtensionFilter txtFilter = new FileNameExtensionFilter("Plain Text (*.txt)", "txt");
|
||||||
|
fc.addChoosableFileFilter(txtFilter);
|
||||||
|
fc.setFileFilter(txtFilter);
|
||||||
|
fc.setSelectedFile(new File("screen_capture.txt"));
|
||||||
|
|
||||||
|
int result = fc.showSaveDialog(parent);
|
||||||
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
|
File file = fc.getSelectedFile();
|
||||||
|
String name = file.getName().toLowerCase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (name.endsWith(".png") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("PNG"))) {
|
||||||
|
if (!name.endsWith(".png")) file = new File(file.getAbsolutePath() + ".png");
|
||||||
|
exportToPng(panel, file);
|
||||||
|
} else if (name.endsWith(".html") || name.endsWith(".htm") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("HTML"))) {
|
||||||
|
if (!name.endsWith(".html") && !name.endsWith(".htm")) file = new File(file.getAbsolutePath() + ".html");
|
||||||
|
exportToHtml(client, file);
|
||||||
|
} else {
|
||||||
|
if (!name.endsWith(".txt")) file = new File(file.getAbsolutePath() + ".txt");
|
||||||
|
exportToText(client, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(parent, "Screen saved successfully to:\n" + file.getAbsolutePath(),
|
||||||
|
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(parent, "Failed to export screen: " + ex.getMessage(),
|
||||||
|
"Export Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void exportToText(Telnet3270Client client, File file) throws IOException {
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int rows = sb.getDisplayRows();
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
|
||||||
|
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||||
|
for (int r = 0; r < rows; r++) {
|
||||||
|
StringBuilder line = new StringBuilder();
|
||||||
|
for (int c = 0; c < cols; c++) {
|
||||||
|
ExtendedAttribute ea = sb.getCell(r * cols + c);
|
||||||
|
char ch = ea.ucs4;
|
||||||
|
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||||
|
line.append(' ');
|
||||||
|
} else {
|
||||||
|
line.append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Strip trailing spaces for clean text output
|
||||||
|
int lastNonSpace = line.length() - 1;
|
||||||
|
while (lastNonSpace >= 0 && line.charAt(lastNonSpace) == ' ') {
|
||||||
|
lastNonSpace--;
|
||||||
|
}
|
||||||
|
writer.write(line.substring(0, lastNonSpace + 1));
|
||||||
|
writer.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void exportToHtml(Telnet3270Client client, File file) throws IOException {
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int rows = sb.getDisplayRows();
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
|
||||||
|
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||||
|
writer.write("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>3270 Screen Capture</title>\n");
|
||||||
|
writer.write("<style>\n");
|
||||||
|
writer.write("body { background-color: #0a0a0a; color: #32cd32; font-family: 'Courier New', Courier, monospace; font-size: 14px; margin: 20px; }\n");
|
||||||
|
writer.write(".screen { background-color: #000; padding: 15px; border-radius: 4px; display: inline-block; box-shadow: 0 0 10px rgba(0,0,0,0.8); line-height: 1.2; }\n");
|
||||||
|
writer.write(".c-blue { color: #5078ff; }\n");
|
||||||
|
writer.write(".c-red { color: #ff3232; }\n");
|
||||||
|
writer.write(".c-pink { color: #ff82b4; }\n");
|
||||||
|
writer.write(".c-green { color: #32cd32; }\n");
|
||||||
|
writer.write(".c-turq { color: #40e0d0; }\n");
|
||||||
|
writer.write(".c-yellow { color: #ffff50; }\n");
|
||||||
|
writer.write(".c-white { color: #ffffff; }\n");
|
||||||
|
writer.write(".c-black { color: #000000; }\n");
|
||||||
|
writer.write(".c-orange { color: #ffa500; }\n");
|
||||||
|
writer.write(".c-purple { color: #b482ff; }\n");
|
||||||
|
writer.write(".c-palegreen { color: #90ee90; }\n");
|
||||||
|
writer.write(".c-paleturq { color: #afeeee; }\n");
|
||||||
|
writer.write(".c-grey { color: #aaaaaa; }\n");
|
||||||
|
writer.write(".bold { font-weight: bold; }\n");
|
||||||
|
writer.write(".underline { text-decoration: underline; }\n");
|
||||||
|
writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>");
|
||||||
|
|
||||||
|
byte currentFA = 0;
|
||||||
|
ExtendedAttribute currentFieldEa = null;
|
||||||
|
|
||||||
|
for (int r = 0; r < rows; r++) {
|
||||||
|
String currentStyle = null;
|
||||||
|
StringBuilder currentRun = new StringBuilder();
|
||||||
|
|
||||||
|
for (int c = 0; c < cols; c++) {
|
||||||
|
int baddr = r * cols + c;
|
||||||
|
ExtendedAttribute ea = sb.getCell(baddr);
|
||||||
|
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
currentFA = ea.fa;
|
||||||
|
currentFieldEa = ea;
|
||||||
|
if (currentRun.length() > 0) {
|
||||||
|
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||||
|
currentRun.setLength(0);
|
||||||
|
currentStyle = null;
|
||||||
|
}
|
||||||
|
writer.write(' ');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faIsZero(currentFA & 0xFF)) {
|
||||||
|
if (currentRun.length() > 0) {
|
||||||
|
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||||
|
currentRun.setLength(0);
|
||||||
|
currentStyle = null;
|
||||||
|
}
|
||||||
|
writer.write(' ');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char ch = ea.ucs4;
|
||||||
|
if (ch == 0 || ch == 0xFF) {
|
||||||
|
if (currentRun.length() > 0) {
|
||||||
|
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||||
|
currentRun.setLength(0);
|
||||||
|
currentStyle = null;
|
||||||
|
}
|
||||||
|
writer.write(' ');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String colorClass = getHtmlColorClass(ea, currentFieldEa, currentFA);
|
||||||
|
boolean bold = faIsHigh(currentFA & 0xFF) || (ea.gr & GR_INTENSIFY) != 0;
|
||||||
|
boolean underline = (ea.gr & GR_UNDERLINE) != 0;
|
||||||
|
|
||||||
|
StringBuilder css = new StringBuilder();
|
||||||
|
if (!colorClass.isEmpty()) css.append(colorClass).append(" ");
|
||||||
|
if (bold) css.append("bold ");
|
||||||
|
if (underline) css.append("underline ");
|
||||||
|
String style = css.toString().trim();
|
||||||
|
|
||||||
|
if (currentStyle == null || !currentStyle.equals(style)) {
|
||||||
|
if (currentRun.length() > 0) {
|
||||||
|
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||||
|
currentRun.setLength(0);
|
||||||
|
}
|
||||||
|
currentStyle = style;
|
||||||
|
}
|
||||||
|
currentRun.append(escapeHtml(ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRun.length() > 0) {
|
||||||
|
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||||
|
}
|
||||||
|
writer.newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write("</pre></div>\n</body>\n</html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeHtmlSpan(BufferedWriter writer, String style, String content) throws IOException {
|
||||||
|
if (style != null && !style.isEmpty()) {
|
||||||
|
writer.write("<span class=\"" + style + "\">" + content + "</span>");
|
||||||
|
} else {
|
||||||
|
writer.write(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getHtmlColorClass(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||||
|
int fg = ea.fg != 0 ? (ea.fg & 0xFF) : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
|
||||||
|
if (fg >= 0xf0 && fg <= 0xff) {
|
||||||
|
switch (fg - 0xf0) {
|
||||||
|
case 1: return "c-blue";
|
||||||
|
case 2: return "c-red";
|
||||||
|
case 3: return "c-pink";
|
||||||
|
case 4: return "c-green";
|
||||||
|
case 5: return "c-turq";
|
||||||
|
case 6: return "c-yellow";
|
||||||
|
case 7: return "c-white";
|
||||||
|
case 8: return "c-black";
|
||||||
|
case 9: return "c-blue";
|
||||||
|
case 10: return "c-orange";
|
||||||
|
case 11: return "c-purple";
|
||||||
|
case 12: return "c-palegreen";
|
||||||
|
case 13: return "c-paleturq";
|
||||||
|
case 14: return "c-grey";
|
||||||
|
case 15: return "c-white";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (faIsProtected(currentFA & 0xFF)) {
|
||||||
|
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue";
|
||||||
|
}
|
||||||
|
return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeHtml(char ch) {
|
||||||
|
switch (ch) {
|
||||||
|
case '<': return "<";
|
||||||
|
case '>': return ">";
|
||||||
|
case '&': return "&";
|
||||||
|
case '"': return """;
|
||||||
|
default: return String.valueOf(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void exportToPng(TerminalPanel panel, File file) throws IOException {
|
||||||
|
int w = panel.getWidth();
|
||||||
|
int h = panel.getHeight();
|
||||||
|
if (w <= 0 || h <= 0) {
|
||||||
|
Dimension pref = panel.getPreferredSize();
|
||||||
|
w = Math.max(pref.width, 800);
|
||||||
|
h = Math.max(pref.height, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g2 = image.createGraphics();
|
||||||
|
panel.paint(g2);
|
||||||
|
g2.dispose();
|
||||||
|
|
||||||
|
ImageIO.write(image, "PNG", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.border.LineBorder;
|
||||||
|
import javax.swing.border.TitledBorder;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for composing and executing Host On-Demand ECL mnemonic keystroke scripts.
|
||||||
|
*/
|
||||||
|
public class ScriptDialog extends JDialog {
|
||||||
|
|
||||||
|
private final Telnet3270Client client;
|
||||||
|
private final TerminalPanel terminalPanel;
|
||||||
|
|
||||||
|
private JTextArea scriptArea;
|
||||||
|
private JLabel statusLabel;
|
||||||
|
|
||||||
|
public ScriptDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||||
|
super(parent, "Run Keystrokes / ECL Script", false);
|
||||||
|
this.client = client;
|
||||||
|
this.terminalPanel = terminalPanel;
|
||||||
|
|
||||||
|
buildUI();
|
||||||
|
setSize(600, 480);
|
||||||
|
setLocationRelativeTo(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||||
|
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);
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(scriptArea);
|
||||||
|
scrollPane.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
new LineBorder(new Color(60, 60, 60)),
|
||||||
|
new EmptyBorder(2, 2, 2, 2)));
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
String[] tokens = {
|
||||||
|
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
|
||||||
|
"[eraseeof]", "[eraseinput]", "[newline]", "[dup]", "[fm]",
|
||||||
|
"[attn]", "[sysreq]", "[cursel]",
|
||||||
|
"[pf1]", "[pf2]", "[pf3]", "[pf4]", "[pf5]", "[pf6]",
|
||||||
|
"[pf7]", "[pf8]", "[pf9]", "[pf10]", "[pf11]", "[pf12]",
|
||||||
|
"[pf13]", "[pf14]", "[pf15]", "[pf16]", "[pf17]", "[pf18]",
|
||||||
|
"[pf19]", "[pf20]", "[pf21]", "[pf22]", "[pf23]", "[pf24]",
|
||||||
|
"[pa1]", "[pa2]", "[pa3]", "[up]", "[down]", "[left]", "[right]", "[home]"
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
btn.setFocusable(false);
|
||||||
|
btn.setMargin(new Insets(2, 4, 2, 4));
|
||||||
|
btn.addActionListener(e -> insertToken(token));
|
||||||
|
tokenPanel.add(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
|
||||||
|
centerPanel.setOpaque(false);
|
||||||
|
centerPanel.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
centerPanel.add(tokenPanel, BorderLayout.SOUTH);
|
||||||
|
mainPanel.add(centerPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Bottom
|
||||||
|
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||||
|
bottomPanel.setOpaque(false);
|
||||||
|
|
||||||
|
statusLabel = new JLabel("Ready");
|
||||||
|
statusLabel.setForeground(new Color(160, 160, 160));
|
||||||
|
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||||
|
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||||
|
|
||||||
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||||
|
buttonPanel.setOpaque(false);
|
||||||
|
|
||||||
|
JButton loadBtn = new JButton("Load Script...");
|
||||||
|
loadBtn.setBackground(new Color(50, 50, 50));
|
||||||
|
loadBtn.setForeground(fg);
|
||||||
|
loadBtn.setFont(labelFont);
|
||||||
|
loadBtn.addActionListener(e -> loadScript());
|
||||||
|
|
||||||
|
JButton saveBtn = new JButton("Save Script...");
|
||||||
|
saveBtn.setBackground(new Color(50, 50, 50));
|
||||||
|
saveBtn.setForeground(fg);
|
||||||
|
saveBtn.setFont(labelFont);
|
||||||
|
saveBtn.addActionListener(e -> saveScript());
|
||||||
|
|
||||||
|
JButton runBtn = new JButton("Execute");
|
||||||
|
runBtn.setBackground(new Color(50, 120, 50));
|
||||||
|
runBtn.setForeground(Color.WHITE);
|
||||||
|
runBtn.setFont(labelFont);
|
||||||
|
runBtn.addActionListener(e -> executeScript());
|
||||||
|
|
||||||
|
JButton closeBtn = new JButton("Close");
|
||||||
|
closeBtn.setBackground(new Color(60, 60, 60));
|
||||||
|
closeBtn.setForeground(fg);
|
||||||
|
closeBtn.setFont(labelFont);
|
||||||
|
closeBtn.addActionListener(e -> dispose());
|
||||||
|
|
||||||
|
buttonPanel.add(loadBtn);
|
||||||
|
buttonPanel.add(saveBtn);
|
||||||
|
buttonPanel.add(Box.createHorizontalStrut(10));
|
||||||
|
buttonPanel.add(runBtn);
|
||||||
|
buttonPanel.add(closeBtn);
|
||||||
|
|
||||||
|
bottomPanel.add(buttonPanel, BorderLayout.EAST);
|
||||||
|
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
setContentPane(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertToken(String token) {
|
||||||
|
scriptArea.replaceSelection(token);
|
||||||
|
scriptArea.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeScript() {
|
||||||
|
String script = scriptArea.getText();
|
||||||
|
if (script == null || script.trim().isEmpty()) {
|
||||||
|
statusLabel.setText("Script is empty.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||||
|
statusLabel.setText("Not connected to host.");
|
||||||
|
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before executing scripts.",
|
||||||
|
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel.setText("Executing keystrokes...");
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
// Split multi-line scripts or process sequentially
|
||||||
|
String cleaned = script.replace("\r\n", "\n").replace("\r", "\n");
|
||||||
|
String[] lines = cleaned.split("\n");
|
||||||
|
for (int i = 0; i < lines.length; i++) {
|
||||||
|
String line = lines[i].trim();
|
||||||
|
if (!line.isEmpty() && !line.startsWith("#") && !line.startsWith("//")) {
|
||||||
|
client.sendKeys(line);
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
statusLabel.setText("Script execution completed successfully.");
|
||||||
|
if (terminalPanel != null) {
|
||||||
|
terminalPanel.repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
statusLabel.setText("Error executing script: " + ex.getMessage());
|
||||||
|
JOptionPane.showMessageDialog(this, "Execution error: " + ex.getMessage(),
|
||||||
|
"Script Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, "ScriptRunner").start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadScript() {
|
||||||
|
JFileChooser fc = new JFileChooser();
|
||||||
|
fc.setDialogTitle("Open Script File");
|
||||||
|
if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = java.nio.file.Files.readAllBytes(fc.getSelectedFile().toPath());
|
||||||
|
scriptArea.setText(new String(bytes, StandardCharsets.UTF_8));
|
||||||
|
statusLabel.setText("Loaded script: " + fc.getSelectedFile().getName());
|
||||||
|
} catch (IOException e) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Could not open file: " + e.getMessage(),
|
||||||
|
"Open Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveScript() {
|
||||||
|
JFileChooser fc = new JFileChooser();
|
||||||
|
fc.setDialogTitle("Save Script File");
|
||||||
|
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
|
try {
|
||||||
|
java.nio.file.Files.write(fc.getSelectedFile().toPath(),
|
||||||
|
scriptArea.getText().getBytes(StandardCharsets.UTF_8));
|
||||||
|
statusLabel.setText("Saved script: " + fc.getSelectedFile().getName());
|
||||||
|
} catch (IOException e) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Could not save file: " + e.getMessage(),
|
||||||
|
"Save Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlowLayout subclass that fully supports word wrapping inside JScrollPanes / dialogs.
|
||||||
|
*/
|
||||||
|
private static class WrapLayout extends FlowLayout {
|
||||||
|
public WrapLayout(int align, int hgap, int vgap) {
|
||||||
|
super(align, hgap, vgap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension preferredLayoutSize(Container target) {
|
||||||
|
return layoutSize(target, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension minimumLayoutSize(Container target) {
|
||||||
|
Dimension minimum = layoutSize(target, false);
|
||||||
|
minimum.width -= (getHgap() + 1);
|
||||||
|
return minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dimension layoutSize(Container target, boolean preferred) {
|
||||||
|
synchronized (target.getTreeLock()) {
|
||||||
|
int targetWidth = target.getWidth();
|
||||||
|
if (targetWidth == 0) targetWidth = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
int hgap = getHgap();
|
||||||
|
int vgap = getVgap();
|
||||||
|
Insets insets = target.getInsets();
|
||||||
|
int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
|
||||||
|
int maxWidth = targetWidth - horizontalInsetsAndGap;
|
||||||
|
|
||||||
|
Dimension dim = new Dimension(0, 0);
|
||||||
|
int rowWidth = 0;
|
||||||
|
int rowHeight = 0;
|
||||||
|
|
||||||
|
int nmembers = target.getComponentCount();
|
||||||
|
for (int i = 0; i < nmembers; i++) {
|
||||||
|
Component m = target.getComponent(i);
|
||||||
|
if (m.isVisible()) {
|
||||||
|
Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
|
||||||
|
if (rowWidth + d.width > maxWidth) {
|
||||||
|
addRow(dim, rowWidth, rowHeight);
|
||||||
|
rowWidth = 0;
|
||||||
|
rowHeight = 0;
|
||||||
|
}
|
||||||
|
if (rowWidth != 0) rowWidth += hgap;
|
||||||
|
rowWidth += d.width;
|
||||||
|
rowHeight = Math.max(rowHeight, d.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addRow(dim, rowWidth, rowHeight);
|
||||||
|
|
||||||
|
dim.width += horizontalInsetsAndGap;
|
||||||
|
dim.height += insets.top + insets.bottom + vgap * 2;
|
||||||
|
return dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addRow(Dimension dim, int rowWidth, int rowHeight) {
|
||||||
|
dim.width = Math.max(dim.width, rowWidth);
|
||||||
|
if (dim.height > 0) dim.height += getVgap();
|
||||||
|
dim.height += rowHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,25 +2,26 @@ package haus.nightmare.j3270.ui;
|
|||||||
|
|
||||||
import haus.nightmare.lib3270j.ConnectionState;
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status bar displaying connection state, cursor position, timing, and lock
|
* Status bar displaying connection state, cursor position, timing, and lock status.
|
||||||
* status.
|
|
||||||
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
|
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
|
||||||
*/
|
*/
|
||||||
public class StatusBar extends JPanel {
|
public class StatusBar extends JPanel {
|
||||||
|
|
||||||
private final JLabel connectionStatus;
|
private final JLabel connectionStatus;
|
||||||
private final JLabel tlsStatus;
|
private final JLabel tlsStatus;
|
||||||
private final JLabel cursorPosition;
|
|
||||||
private final JLabel luName;
|
private final JLabel luName;
|
||||||
private final JLabel lockStatus;
|
private final JLabel lockStatus;
|
||||||
|
private final JLabel fieldTypeStatus;
|
||||||
|
private final JLabel codePageInfo;
|
||||||
private final JLabel modelInfo;
|
private final JLabel modelInfo;
|
||||||
private final JButton lpButton;
|
private final JLabel cursorPosition;
|
||||||
|
|
||||||
private Telnet3270Client client;
|
private Telnet3270Client client;
|
||||||
private TerminalPanel terminalPanel;
|
private TerminalPanel terminalPanel;
|
||||||
@@ -30,6 +31,7 @@ public class StatusBar extends JPanel {
|
|||||||
private static final Color OIA_FG = new Color(50, 205, 50);
|
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_DIM = new Color(80, 80, 80);
|
||||||
private static final Color OIA_ALERT = new Color(255, 80, 80);
|
private static final Color OIA_ALERT = new Color(255, 80, 80);
|
||||||
|
private static final Color OIA_WARN = new Color(255, 200, 80);
|
||||||
|
|
||||||
public StatusBar() {
|
public StatusBar() {
|
||||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||||
@@ -43,33 +45,24 @@ public class StatusBar extends JPanel {
|
|||||||
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
||||||
luName = createLabel("", oiaFont, OIA_FG);
|
luName = createLabel("", oiaFont, OIA_FG);
|
||||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||||
|
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
|
||||||
|
codePageInfo = createLabel("", oiaFont, OIA_DIM);
|
||||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||||
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
|
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG);
|
||||||
|
|
||||||
lpButton = new JButton("LightPen: OFF");
|
|
||||||
lpButton.setFont(oiaFont);
|
|
||||||
lpButton.setForeground(OIA_FG);
|
|
||||||
lpButton.setBackground(OIA_BG);
|
|
||||||
lpButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
|
|
||||||
lpButton.setFocusable(false);
|
|
||||||
lpButton.addActionListener(e -> {
|
|
||||||
if (terminalPanel != null) {
|
|
||||||
terminalPanel.toggleLightPen();
|
|
||||||
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
add(Box.createHorizontalStrut(6));
|
add(Box.createHorizontalStrut(6));
|
||||||
add(connectionStatus);
|
add(connectionStatus);
|
||||||
add(Box.createHorizontalStrut(10));
|
add(Box.createHorizontalStrut(10));
|
||||||
add(tlsStatus);
|
add(tlsStatus);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(10));
|
||||||
add(luName);
|
add(luName);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
add(lockStatus);
|
add(lockStatus);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(10));
|
||||||
add(lpButton);
|
add(fieldTypeStatus);
|
||||||
add(Box.createHorizontalGlue());
|
add(Box.createHorizontalGlue());
|
||||||
|
add(codePageInfo);
|
||||||
|
add(Box.createHorizontalStrut(12));
|
||||||
add(modelInfo);
|
add(modelInfo);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
add(cursorPosition);
|
add(cursorPosition);
|
||||||
@@ -86,15 +79,22 @@ public class StatusBar extends JPanel {
|
|||||||
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.terminalPanel = terminalPanel;
|
this.terminalPanel = terminalPanel;
|
||||||
if (terminalPanel != null) {
|
|
||||||
terminalPanel.setOnLightPenToggle(this::updateStatus);
|
|
||||||
}
|
|
||||||
updateStatus();
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateStatus() {
|
public void updateStatus() {
|
||||||
if (client == null)
|
if (client == null) {
|
||||||
|
connectionStatus.setText("Not Connected");
|
||||||
|
connectionStatus.setForeground(OIA_DIM);
|
||||||
|
tlsStatus.setText("");
|
||||||
|
luName.setText("");
|
||||||
|
lockStatus.setText("");
|
||||||
|
fieldTypeStatus.setText("");
|
||||||
|
codePageInfo.setText("");
|
||||||
|
modelInfo.setText("");
|
||||||
|
cursorPosition.setText("001/001");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Connection state
|
// Connection state
|
||||||
ConnectionState state = client.getConnectionState();
|
ConnectionState state = client.getConnectionState();
|
||||||
@@ -128,7 +128,7 @@ public class StatusBar extends JPanel {
|
|||||||
break;
|
break;
|
||||||
case CONNECTED_UNBOUND:
|
case CONNECTED_UNBOUND:
|
||||||
connectionStatus.setText("Unbound");
|
connectionStatus.setText("Unbound");
|
||||||
connectionStatus.setForeground(new Color(255, 255, 80));
|
connectionStatus.setForeground(OIA_WARN);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
connectionStatus.setText(state.name());
|
connectionStatus.setText(state.name());
|
||||||
@@ -156,16 +156,41 @@ public class StatusBar extends JPanel {
|
|||||||
tlsStatus.setToolTipText(null);
|
tlsStatus.setToolTipText(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LU name
|
// LU Name
|
||||||
String lu = "";
|
String lu = "";
|
||||||
if (client.getConnectionState().isTn3270e()) {
|
if (client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null && !client.getTelnetFSM().getConnectedLu().isEmpty()) {
|
||||||
// lu would come from FSM
|
lu = "LU:" + client.getTelnetFSM().getConnectedLu();
|
||||||
|
} else if (client.getConfig().getLuName() != null && !client.getConfig().getLuName().isEmpty()) {
|
||||||
|
lu = "LU:" + client.getConfig().getLuName();
|
||||||
}
|
}
|
||||||
luName.setText(lu);
|
luName.setText(lu);
|
||||||
|
|
||||||
// Lock status
|
// Lock / Inhibit status
|
||||||
if (client.getInputProcessor().isKeyboardLocked()) {
|
int inhibit = client.getOIA().getInputInhibited();
|
||||||
|
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
|
||||||
|
switch (inhibit) {
|
||||||
|
case ECLConstants.INHIBIT_SYSTEM_LOCK:
|
||||||
lockStatus.setText("X SYSTEM");
|
lockStatus.setText("X SYSTEM");
|
||||||
|
break;
|
||||||
|
case ECLConstants.INHIBIT_COMM_CHECK:
|
||||||
|
lockStatus.setText("X COMM");
|
||||||
|
break;
|
||||||
|
case ECLConstants.INHIBIT_NUMERIC_ONLY:
|
||||||
|
lockStatus.setText("X NUM");
|
||||||
|
break;
|
||||||
|
case ECLConstants.INHIBIT_PROTECTED_FIELD:
|
||||||
|
lockStatus.setText("X PROT");
|
||||||
|
break;
|
||||||
|
case ECLConstants.INHIBIT_OVERFLOW:
|
||||||
|
lockStatus.setText("X >");
|
||||||
|
break;
|
||||||
|
case ECLConstants.INHIBIT_OPERATOR_DUE:
|
||||||
|
lockStatus.setText("X OP");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
lockStatus.setText("X LOCKED");
|
||||||
|
break;
|
||||||
|
}
|
||||||
lockStatus.setForeground(OIA_ALERT);
|
lockStatus.setForeground(OIA_ALERT);
|
||||||
} else if (client.getInputProcessor().isInsertMode()) {
|
} else if (client.getInputProcessor().isInsertMode()) {
|
||||||
lockStatus.setText("INSERT");
|
lockStatus.setText("INSERT");
|
||||||
@@ -174,19 +199,34 @@ public class StatusBar extends JPanel {
|
|||||||
lockStatus.setText("");
|
lockStatus.setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model info
|
// Field status: Numeric vs Alphanumeric
|
||||||
modelInfo.setText(client.getConfig().getModel().getTerminalType());
|
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
|
||||||
|
if (client.getOIA().isNumeric()) {
|
||||||
|
fieldTypeStatus.setText("NUM");
|
||||||
|
fieldTypeStatus.setForeground(OIA_WARN);
|
||||||
|
} else {
|
||||||
|
fieldTypeStatus.setText("ALPHA");
|
||||||
|
fieldTypeStatus.setForeground(OIA_DIM);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldTypeStatus.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
// Cursor position
|
// Active Code Page
|
||||||
|
String cp = client.getCodePage();
|
||||||
|
codePageInfo.setText(cp != null ? "CP" + cp : "");
|
||||||
|
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
|
||||||
|
|
||||||
|
// Model & Dimensions info
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int rows = sb.getDisplayRows();
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
|
||||||
|
|
||||||
|
// Cursor position and buffer address
|
||||||
|
int curAddr = sb.getCursorAddress();
|
||||||
int row = sb.getCursorRow() + 1;
|
int row = sb.getCursorRow() + 1;
|
||||||
int col = sb.getCursorCol() + 1;
|
int col = sb.getCursorCol() + 1;
|
||||||
cursorPosition.setText(String.format("%03d/%03d", row, col));
|
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||||
|
|
||||||
// Light Pen indicator — sync with actual state
|
|
||||||
if (terminalPanel != null) {
|
|
||||||
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
|
|
||||||
lpButton.setForeground(terminalPanel.isLightPenMode() ? new Color(255, 255, 80) : OIA_FG);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package haus.nightmare.j3270.ui;
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
@@ -14,10 +15,10 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom JPanel that renders the 3270 screen buffer.
|
* Custom JPanel that renders the 3270 screen buffer.
|
||||||
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
|
* Supports colors, bold, underline, reverse, blink, GOCA vector graphics,
|
||||||
* attributes.
|
* programmed symbols, search highlighting, and all 3278/3279 attributes.
|
||||||
*/
|
*/
|
||||||
public class TerminalPanel extends JPanel {
|
public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||||
|
|
||||||
private Telnet3270Client client;
|
private Telnet3270Client client;
|
||||||
|
|
||||||
@@ -62,7 +63,12 @@ public class TerminalPanel extends JPanel {
|
|||||||
private boolean isDragging = false;
|
private boolean isDragging = false;
|
||||||
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
||||||
|
|
||||||
// ========== Resize guard ==========
|
// ========== Search Highlight state ==========
|
||||||
|
private int searchHighlightAddr = -1;
|
||||||
|
private int searchHighlightLen = 0;
|
||||||
|
private static final Color SEARCH_HIGHLIGHT_COLOR = new Color(255, 215, 0, 120);
|
||||||
|
|
||||||
|
// ========== Graphics / Resize guard ==========
|
||||||
private boolean lightPenMode = false;
|
private boolean lightPenMode = false;
|
||||||
private long lastGraphicsUpdateCount = -1;
|
private long lastGraphicsUpdateCount = -1;
|
||||||
private java.awt.image.BufferedImage cachedGraphicsImage = null;
|
private java.awt.image.BufferedImage cachedGraphicsImage = null;
|
||||||
@@ -70,7 +76,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the horizontal render offset to center the grid within the panel.
|
* Compute the horizontal render offset to center the grid within the panel.
|
||||||
* Any leftover pixels (from integer font sizing) are split evenly.
|
|
||||||
*/
|
*/
|
||||||
private int getRenderOffsetX() {
|
private int getRenderOffsetX() {
|
||||||
int termCols = 80;
|
int termCols = 80;
|
||||||
@@ -82,7 +87,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the vertical render offset to center the grid within the panel.
|
* Compute the vertical render offset to center the grid within the panel.
|
||||||
* Must use getDisplayRows() to match paintComponent's iteration.
|
|
||||||
*/
|
*/
|
||||||
private int getRenderOffsetY() {
|
private int getRenderOffsetY() {
|
||||||
int termRows = 24;
|
int termRows = 24;
|
||||||
@@ -128,8 +132,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
setDoubleBuffered(true);
|
setDoubleBuffered(true);
|
||||||
setFocusTraversalKeysEnabled(false);
|
setFocusTraversalKeysEnabled(false);
|
||||||
|
|
||||||
// Use key bindings instead of KeyListener for reliable key handling
|
|
||||||
// This avoids focus/event issues with JScrollPane
|
|
||||||
setOpaque(true);
|
setOpaque(true);
|
||||||
setupKeyBindings();
|
setupKeyBindings();
|
||||||
setupFont();
|
setupFont();
|
||||||
@@ -160,12 +162,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
col = Math.max(0, Math.min(col, displayCols - 1));
|
col = Math.max(0, Math.min(col, displayCols - 1));
|
||||||
row = Math.max(0, Math.min(row, displayRows - 1));
|
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||||
|
|
||||||
// IMPORTANT ARCHITECTURE NOTE:
|
|
||||||
// 1. In GDDM Graphic Cursor Mode (3179G / GOCA), mouse events represent graphic light-pen touches.
|
|
||||||
// Text drag-selection is explicitly suppressed so blue selection boxes do not artifact over graphics.
|
|
||||||
// 2. In Text Light Pen / Selectable Field mode (per IBM Host On-Demand MouseMgr.java / PS3270.java),
|
|
||||||
// clicks on detectable fields (faIsSelectable) trigger field selection or cursor select.
|
|
||||||
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
|
|
||||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||||
boolean isSelectableField = false;
|
boolean isSelectableField = false;
|
||||||
if (sb.isFormatted()) {
|
if (sb.isFormatted()) {
|
||||||
@@ -267,16 +263,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||||
|
|
||||||
int gx = client.getGocaDecoder().getGraphicCursorX();
|
|
||||||
int gy = client.getGocaDecoder().getGraphicCursorY();
|
|
||||||
int hitSeg = client.getGocaDecoder().findPickedSegment(gx, gy);
|
|
||||||
int hitTag = client.getGocaDecoder().getSegmentTag(hitSeg);
|
|
||||||
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
||||||
System.err.println(String.format(
|
|
||||||
"TerminalPanel.mouseReleased: mouse=(%d, %d) offset=(%d, %d) grid=(%dx%d) px=(%d, %d) goca=(%d, %d) hitSeg=%d hitTag=%d btn=%d",
|
|
||||||
e.getX(), e.getY(), ox, oy, gridW, gridH, px, py, gx, gy, hitSeg, hitTag, button
|
|
||||||
));
|
|
||||||
|
|
||||||
client.getInputProcessor().sendGraphicMouseAid(
|
client.getInputProcessor().sendGraphicMouseAid(
|
||||||
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
||||||
button,
|
button,
|
||||||
@@ -288,9 +275,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Automatic Light-Pen / Selectable Field detection on mouse click:
|
// Automatic Light-Pen / Selectable Field detection on mouse click:
|
||||||
// Per IBM Host On-Demand (MouseMgr.java / PS3270.java) and 3270 specifications:
|
|
||||||
// If the clicked screen location belongs to a detectable field (faIsSelectable and not faIsZero),
|
|
||||||
// automatically process light-pen / cursor selection (e.g. immediate selection, Enter, or toggling ? -> >).
|
|
||||||
if (sb.isFormatted()) {
|
if (sb.isFormatted()) {
|
||||||
int faPos = sb.findFieldAttribute(clickAddr);
|
int faPos = sb.findFieldAttribute(clickAddr);
|
||||||
if (faPos >= 0) {
|
if (faPos >= 0) {
|
||||||
@@ -344,23 +328,42 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
// ========== Selection / Copy-Paste ==========
|
// ========== Selection / Copy-Paste ==========
|
||||||
|
|
||||||
private void clearSelection() {
|
public void clearSelection() {
|
||||||
selectionStartRow = -1;
|
selectionStartRow = -1;
|
||||||
selectionStartCol = -1;
|
selectionStartCol = -1;
|
||||||
selectionEndRow = -1;
|
selectionEndRow = -1;
|
||||||
selectionEndCol = -1;
|
selectionEndCol = -1;
|
||||||
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasSelection() {
|
public void selectAll() {
|
||||||
|
if (client == null) return;
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
selectionStartRow = 0;
|
||||||
|
selectionStartCol = 0;
|
||||||
|
selectionEndRow = sb.getDisplayRows() - 1;
|
||||||
|
selectionEndCol = sb.getDisplayCols() - 1;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectionRange(int startAddr, int endAddr) {
|
||||||
|
if (client == null) return;
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
if (cols <= 0) return;
|
||||||
|
|
||||||
|
selectionStartRow = startAddr / cols;
|
||||||
|
selectionStartCol = startAddr % cols;
|
||||||
|
selectionEndRow = endAddr / cols;
|
||||||
|
selectionEndCol = endAddr % cols;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSelection() {
|
||||||
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
|
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
|
||||||
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
|
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the selected text from the screen buffer.
|
|
||||||
* In block mode: rectangular selection with newlines between rows.
|
|
||||||
* In line mode: stream selection, flowing left-to-right, top-to-bottom.
|
|
||||||
*/
|
|
||||||
public String getSelectedText() {
|
public String getSelectedText() {
|
||||||
if (!hasSelection() || client == null) return "";
|
if (!hasSelection() || client == null) return "";
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
@@ -371,7 +374,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
int c1, c2;
|
int c1, c2;
|
||||||
|
|
||||||
if (blockSelectMode) {
|
if (blockSelectMode) {
|
||||||
// Block mode: rectangular selection
|
|
||||||
c1 = Math.min(selectionStartCol, selectionEndCol);
|
c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||||
c2 = Math.max(selectionStartCol, selectionEndCol);
|
c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||||
|
|
||||||
@@ -391,7 +393,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
return result.toString();
|
return result.toString();
|
||||||
} else {
|
} else {
|
||||||
// Line/stream mode: flow from start to end
|
|
||||||
int startAddr, endAddr;
|
int startAddr, endAddr;
|
||||||
if (selectionStartRow < selectionEndRow ||
|
if (selectionStartRow < selectionEndRow ||
|
||||||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||||
@@ -422,7 +423,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copySelection() {
|
public void copySelection() {
|
||||||
String text = getSelectedText();
|
String text = getSelectedText();
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
StringSelection ss = new StringSelection(text);
|
StringSelection ss = new StringSelection(text);
|
||||||
@@ -430,7 +431,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pasteClipboard() {
|
public void pasteClipboard() {
|
||||||
if (client == null || !client.getConnectionState().isFullSession()) return;
|
if (client == null || !client.getConnectionState().isFullSession()) return;
|
||||||
try {
|
try {
|
||||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
@@ -438,8 +439,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (text != null) {
|
if (text != null) {
|
||||||
for (char ch : text.toCharArray()) {
|
for (char ch : text.toCharArray()) {
|
||||||
if (ch == '\n' || ch == '\r') {
|
if (ch == '\n' || ch == '\r') {
|
||||||
// Skip newlines in paste — user may paste multi-line text
|
|
||||||
// but 3270 fields don't wrap the same way
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ch >= 0x20 && ch != 0x7F) {
|
if (ch >= 0x20 && ch != 0x7F) {
|
||||||
@@ -448,9 +447,35 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ignored) {}
|
||||||
// Clipboard not available or wrong type — silently ignore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void pasteLineWrap(String text, int endCol, boolean wordWrap) {
|
||||||
|
if (client == null || text == null || text.isEmpty()) return;
|
||||||
|
int curPos = client.getScreenBuffer().getCursorAddress();
|
||||||
|
client.getPS().pasteLineWrap(text, curPos, endCol, wordWrap);
|
||||||
|
refreshScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Search Highlighting ==========
|
||||||
|
|
||||||
|
public void setSearchHighlight(int addr, int len) {
|
||||||
|
this.searchHighlightAddr = addr;
|
||||||
|
this.searchHighlightLen = len;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSearchHighlight() {
|
||||||
|
this.searchHighlightAddr = -1;
|
||||||
|
this.searchHighlightLen = 0;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCellSearchHighlighted(int row, int col) {
|
||||||
|
if (searchHighlightAddr < 0 || searchHighlightLen <= 0 || client == null) return false;
|
||||||
|
int cols = client.getScreenBuffer().getDisplayCols();
|
||||||
|
int addr = row * cols + col;
|
||||||
|
return addr >= searchHighlightAddr && addr < searchHighlightAddr + searchHighlightLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showContextMenu(MouseEvent e) {
|
private void showContextMenu(MouseEvent e) {
|
||||||
@@ -466,6 +491,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
pasteItem.addActionListener(ev -> pasteClipboard());
|
pasteItem.addActionListener(ev -> pasteClipboard());
|
||||||
popup.add(pasteItem);
|
popup.add(pasteItem);
|
||||||
|
|
||||||
|
JMenuItem selectAllItem = new JMenuItem("Select All");
|
||||||
|
selectAllItem.addActionListener(ev -> selectAll());
|
||||||
|
popup.add(selectAllItem);
|
||||||
|
|
||||||
popup.addSeparator();
|
popup.addSeparator();
|
||||||
|
|
||||||
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||||
@@ -489,7 +518,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||||
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
||||||
} else {
|
} else {
|
||||||
// Stream mode
|
|
||||||
int cols = 80;
|
int cols = 80;
|
||||||
if (client != null) cols = client.getScreenBuffer().getCols();
|
if (client != null) cols = client.getScreenBuffer().getCols();
|
||||||
int addr = row * cols + col;
|
int addr = row * cols + col;
|
||||||
@@ -508,12 +536,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
// ========== Font auto-resize ==========
|
// ========== Font auto-resize ==========
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-fit the font size to fill the current panel dimensions
|
|
||||||
* while respecting the terminal model's character grid.
|
|
||||||
* Any leftover pixels are handled by centering the grid
|
|
||||||
* (see getRenderOffsetX/Y).
|
|
||||||
*/
|
|
||||||
private void autoFitFont() {
|
private void autoFitFont() {
|
||||||
int panelW = getWidth();
|
int panelW = getWidth();
|
||||||
int panelH = getHeight();
|
int panelH = getHeight();
|
||||||
@@ -527,12 +549,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
termRows = sb.getDisplayRows();
|
termRows = sb.getDisplayRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use minimal padding for the fit calculation
|
|
||||||
int availW = panelW - 2 * padding;
|
int availW = panelW - 2 * padding;
|
||||||
int availH = panelH - 2 * padding;
|
int availH = panelH - 2 * padding;
|
||||||
if (availW <= 0 || availH <= 0) return;
|
if (availW <= 0 || availH <= 0) return;
|
||||||
|
|
||||||
// Find the largest font size where the grid fits
|
|
||||||
int bestSize = 8;
|
int bestSize = 8;
|
||||||
for (int testSize = 8; testSize <= 72; testSize++) {
|
for (int testSize = 8; testSize <= 72; testSize++) {
|
||||||
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
|
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
|
||||||
@@ -553,15 +573,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
||||||
updateCellSize();
|
updateCellSize();
|
||||||
}
|
}
|
||||||
// No window snap — any leftover pixels are centered via getRenderOffsetX/Y
|
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by J3270App when it needs to pack the frame (screen size changed,
|
|
||||||
* connection established, etc.). Sets the resize guard to prevent
|
|
||||||
* autoFitFont from firing during the pack.
|
|
||||||
*/
|
|
||||||
public void guardedPack() {
|
public void guardedPack() {
|
||||||
resizeGuard = true;
|
resizeGuard = true;
|
||||||
revalidate();
|
revalidate();
|
||||||
@@ -572,16 +586,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Use InputMap/ActionMap (key bindings) instead of KeyListener.
|
|
||||||
* This works reliably even inside a JScrollPane — the WHEN_FOCUSED
|
|
||||||
* condition ensures our panel receives all key events when focused.
|
|
||||||
*/
|
|
||||||
private void bindKeyToMap(InputMap im, String action, String bindingStr) {
|
private void bindKeyToMap(InputMap im, String action, String bindingStr) {
|
||||||
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
|
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Support multiple bindings separated by commas
|
|
||||||
String[] bindings = bindingStr.split(",");
|
String[] bindings = bindingStr.split(",");
|
||||||
for (String binding : bindings) {
|
for (String binding : bindings) {
|
||||||
binding = binding.trim();
|
binding = binding.trim();
|
||||||
@@ -601,7 +609,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
im.clear();
|
im.clear();
|
||||||
am.clear();
|
am.clear();
|
||||||
|
|
||||||
// Block the scroll pane from handling Tab, arrows, Page keys
|
|
||||||
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||||
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
||||||
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
||||||
@@ -611,25 +618,16 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PF keys 1-24
|
// PF keys 1-24
|
||||||
// PF1-12 default to F1-F12, PF13-24 default to shift F1-shift F12
|
|
||||||
for (int i = 1; i <= 24; i++) {
|
for (int i = 1; i <= 24; i++) {
|
||||||
String defaultBinding;
|
String defaultBinding = i <= 12 ? ("F" + i) : ("shift F" + (i - 12));
|
||||||
if (i <= 12) {
|
|
||||||
defaultBinding = "F" + i;
|
|
||||||
} else {
|
|
||||||
defaultBinding = "shift F" + (i - 12);
|
|
||||||
}
|
|
||||||
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
||||||
bindKeyToMap(im, "PF" + i, binding);
|
bindKeyToMap(im, "PF" + i, binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PA keys: Alt+1, Alt+2, Alt+3
|
// PA keys
|
||||||
String pa1Def = "alt 1";
|
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", "alt 1"));
|
||||||
String pa2Def = "alt 2";
|
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", "alt 2"));
|
||||||
String pa3Def = "alt 3";
|
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", "alt 3"));
|
||||||
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
|
|
||||||
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
|
|
||||||
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
|
|
||||||
|
|
||||||
// Clear
|
// Clear
|
||||||
bindKeyToMap(im, "CLEAR", haus.nightmare.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
bindKeyToMap(im, "CLEAR", haus.nightmare.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
||||||
@@ -643,13 +641,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
||||||
bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q"));
|
bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q"));
|
||||||
|
|
||||||
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
|
// Copy/Paste/Lightpen bindings
|
||||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
||||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
|
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
|
||||||
|
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, shortcutMask), "j3270-SELECTALL");
|
||||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
|
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
|
||||||
|
|
||||||
// Create actions for all bound keys
|
// Action map implementations
|
||||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||||
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
||||||
am.put("j3270-TAB", createAction(() -> handleTab(false)));
|
am.put("j3270-TAB", createAction(() -> handleTab(false)));
|
||||||
@@ -674,9 +673,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
||||||
am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
|
am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
|
||||||
|
|
||||||
// Copy/Paste actions
|
|
||||||
am.put("j3270-COPY", createAction(this::copySelection));
|
am.put("j3270-COPY", createAction(this::copySelection));
|
||||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||||
|
am.put("j3270-SELECTALL", createAction(this::selectAll));
|
||||||
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
||||||
|
|
||||||
for (int i = 1; i <= 24; i++) {
|
for (int i = 1; i <= 24; i++) {
|
||||||
@@ -688,18 +687,24 @@ public class TerminalPanel extends JPanel {
|
|||||||
am.put("j3270-PA2", createAction(() -> handlePA(2)));
|
am.put("j3270-PA2", createAction(() -> handlePA(2)));
|
||||||
am.put("j3270-PA3", createAction(() -> handlePA(3)));
|
am.put("j3270-PA3", createAction(() -> handlePA(3)));
|
||||||
|
|
||||||
// For printable character input, we override processKeyEvent
|
|
||||||
enableEvents(AWTEvent.KEY_EVENT_MASK);
|
enableEvents(AWTEvent.KEY_EVENT_MASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void processKeyEvent(KeyEvent e) {
|
protected void processKeyEvent(KeyEvent e) {
|
||||||
// Handle character typing via processKeyEvent to capture ALL typed chars
|
|
||||||
if (e.getID() == KeyEvent.KEY_TYPED) {
|
if (e.getID() == KeyEvent.KEY_TYPED) {
|
||||||
char ch = e.getKeyChar();
|
char ch = e.getKeyChar();
|
||||||
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
||||||
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null) {
|
||||||
|
ConnectionState state = client.getConnectionState();
|
||||||
|
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
|
||||||
|
try {
|
||||||
|
client.sendNVTChar(ch);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
} else if (state.isFullSession()) {
|
||||||
client.typeCharacter(ch);
|
client.typeCharacter(ch);
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
e.consume();
|
e.consume();
|
||||||
@@ -707,6 +712,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
super.processKeyEvent(e);
|
super.processKeyEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,14 +728,23 @@ public class TerminalPanel extends JPanel {
|
|||||||
// ========== Key action handlers ==========
|
// ========== Key action handlers ==========
|
||||||
|
|
||||||
private void handleEnter() {
|
private void handleEnter() {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null) {
|
||||||
|
ConnectionState state = client.getConnectionState();
|
||||||
|
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
|
||||||
|
try {
|
||||||
|
client.sendNVTString("\r\n");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
} else if (state.isFullSession()) {
|
||||||
client.sendEnter();
|
client.sendEnter();
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleReset() {
|
private void handleReset() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
|
clearSearchHighlight();
|
||||||
|
clearSelection();
|
||||||
client.reset();
|
client.reset();
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
@@ -737,10 +752,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
private void handleTab(boolean shift) {
|
private void handleTab(boolean shift) {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
if (shift)
|
if (shift) client.backTab();
|
||||||
client.backTab();
|
else client.tab();
|
||||||
else
|
|
||||||
client.tab();
|
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -748,21 +761,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
private void handleCursor(String dir) {
|
private void handleCursor(String dir) {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
switch (dir) {
|
switch (dir) {
|
||||||
case "up":
|
case "up": client.cursorUp(); break;
|
||||||
client.cursorUp();
|
case "down": client.cursorDown(); break;
|
||||||
break;
|
case "left": client.cursorLeft(); break;
|
||||||
case "down":
|
case "right": client.cursorRight(); break;
|
||||||
client.cursorDown();
|
case "home": client.cursorHome(); break;
|
||||||
break;
|
|
||||||
case "left":
|
|
||||||
client.cursorLeft();
|
|
||||||
break;
|
|
||||||
case "right":
|
|
||||||
client.cursorRight();
|
|
||||||
break;
|
|
||||||
case "home":
|
|
||||||
client.cursorHome();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
@@ -797,11 +800,18 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleBackspace() {
|
private void handleBackspace() {
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null) {
|
||||||
|
ConnectionState state = client.getConnectionState();
|
||||||
|
if (state == ConnectionState.CONNECTED_NVT || state == ConnectionState.CONNECTED_NVT_CHAR || state == ConnectionState.CONNECTED_E_NVT) {
|
||||||
|
try {
|
||||||
|
client.sendNVTChar('\b');
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
} else if (state.isFullSession()) {
|
||||||
client.getInputProcessor().backspace();
|
client.getInputProcessor().backspace();
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleInsert() {
|
private void handleInsert() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
@@ -870,12 +880,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshScreen() {
|
public void refreshScreen() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.getScreenBuffer().updateDisplaySnapshot();
|
client.getScreenBuffer().updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
repaint();
|
repaint();
|
||||||
// Notify parent to update status bar too
|
|
||||||
Container parent = getParent();
|
Container parent = getParent();
|
||||||
while (parent != null) {
|
while (parent != null) {
|
||||||
if (parent instanceof JFrame) {
|
if (parent instanceof JFrame) {
|
||||||
@@ -892,7 +901,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
||||||
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
||||||
// Fallback
|
|
||||||
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
|
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
|
||||||
}
|
}
|
||||||
updateCellSize();
|
updateCellSize();
|
||||||
@@ -1015,9 +1023,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
public Dimension getPreferredSize() {
|
public Dimension getPreferredSize() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
|
|
||||||
// When the host switches to alternate screen, onScreenSizeChanged fires
|
|
||||||
// and the frame re-packs.
|
|
||||||
int displayCols = sb.getDisplayCols();
|
int displayCols = sb.getDisplayCols();
|
||||||
int displayRows = sb.getDisplayRows();
|
int displayRows = sb.getDisplayRows();
|
||||||
return new Dimension(displayCols * cellWidth + padding * 2,
|
return new Dimension(displayCols * cellWidth + padding * 2,
|
||||||
@@ -1042,11 +1047,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||||
|
|
||||||
// Clear entire panel with background color
|
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||||
|
|
||||||
// Compute centered offsets for the grid
|
|
||||||
int ox = getRenderOffsetX();
|
int ox = getRenderOffsetX();
|
||||||
int oy = getRenderOffsetY();
|
int oy = getRenderOffsetY();
|
||||||
|
|
||||||
@@ -1077,7 +1080,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track current field attribute for monochrome color decisions
|
|
||||||
byte currentFA = 0;
|
byte currentFA = 0;
|
||||||
ExtendedAttribute currentFieldEa = null;
|
ExtendedAttribute currentFieldEa = null;
|
||||||
|
|
||||||
@@ -1089,7 +1091,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
int x = ox + col * cellWidth;
|
int x = ox + col * cellWidth;
|
||||||
int y = oy + row * cellHeight;
|
int y = oy + row * cellHeight;
|
||||||
|
|
||||||
// Determine colors and attributes
|
|
||||||
Color fgColor;
|
Color fgColor;
|
||||||
Color bgColor;
|
Color bgColor;
|
||||||
boolean bold = false;
|
boolean bold = false;
|
||||||
@@ -1099,7 +1100,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (ea.isFieldAttribute()) {
|
if (ea.isFieldAttribute()) {
|
||||||
currentFA = ea.fa;
|
currentFA = ea.fa;
|
||||||
currentFieldEa = ea;
|
currentFieldEa = ea;
|
||||||
// Selection highlight on field attribute cells
|
|
||||||
if (isCellSelected(row, col)) {
|
if (isCellSelected(row, col)) {
|
||||||
g2.setColor(SELECTION_COLOR);
|
g2.setColor(SELECTION_COLOR);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
@@ -1119,20 +1119,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Graphics rendition
|
// Graphics rendition
|
||||||
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
|
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
|
||||||
if (gr != 0) {
|
if (gr != 0) {
|
||||||
if ((gr & GR_INTENSIFY) != 0)
|
if ((gr & GR_INTENSIFY) != 0) bold = true;
|
||||||
bold = true;
|
if ((gr & GR_UNDERLINE) != 0) underline = true;
|
||||||
if ((gr & GR_UNDERLINE) != 0)
|
if ((gr & GR_REVERSE) != 0) reverse = true;
|
||||||
underline = true;
|
|
||||||
if ((gr & GR_REVERSE) != 0)
|
|
||||||
reverse = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field attribute implicit intensify
|
if (faIsHigh(currentFA & 0xFF)) bold = true;
|
||||||
if (faIsHigh(currentFA & 0xFF))
|
|
||||||
bold = true;
|
|
||||||
|
|
||||||
// Handle invisible fields (zero intensity / password fields)
|
// Password fields
|
||||||
// Modern UX: render '*' for typed characters so user sees length/digit count
|
|
||||||
if (faIsZero(currentFA & 0xFF)) {
|
if (faIsZero(currentFA & 0xFF)) {
|
||||||
char ch = ea.ucs4;
|
char ch = ea.ucs4;
|
||||||
if (ch > 0x20 && ch != 0xFF) {
|
if (ch > 0x20 && ch != 0xFF) {
|
||||||
@@ -1149,14 +1143,12 @@ public class TerminalPanel extends JPanel {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply reverse video
|
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
Color tmp = fgColor;
|
Color tmp = fgColor;
|
||||||
fgColor = bgColor;
|
fgColor = bgColor;
|
||||||
bgColor = tmp;
|
bgColor = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw background only if different from default panel bgColor or if inverted
|
|
||||||
if (!bgColor.equals(this.bgColor) || reverse) {
|
if (!bgColor.equals(this.bgColor) || reverse) {
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
@@ -1192,13 +1184,18 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw underline
|
|
||||||
if (underline) {
|
if (underline) {
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
||||||
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
|
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw search highlight
|
||||||
|
if (isCellSearchHighlighted(row, col)) {
|
||||||
|
g2.setColor(SEARCH_HIGHLIGHT_COLOR);
|
||||||
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
if (isCellSelected(row, col)) {
|
if (isCellSelected(row, col)) {
|
||||||
g2.setColor(SELECTION_COLOR);
|
g2.setColor(SELECTION_COLOR);
|
||||||
@@ -1207,7 +1204,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Graphic Cursor (Light-Pen / interactive graphics pointer) if active
|
// Draw Graphic Cursor if active
|
||||||
if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
|
if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
|
||||||
int gocaX = client.getGocaDecoder().getGraphicCursorX();
|
int gocaX = client.getGocaDecoder().getGraphicCursorX();
|
||||||
int gocaY = client.getGocaDecoder().getGraphicCursorY();
|
int gocaY = client.getGocaDecoder().getGraphicCursorY();
|
||||||
@@ -1222,7 +1219,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
g2.setColor(Color.WHITE);
|
g2.setColor(Color.WHITE);
|
||||||
g2.setXORMode(Color.BLACK);
|
g2.setXORMode(Color.BLACK);
|
||||||
// Draw a crosshair cursor for the light-pen / graphic cursor
|
|
||||||
g2.drawLine(px - 6, py, px + 6, py);
|
g2.drawLine(px - 6, py, px + 6, py);
|
||||||
g2.drawLine(px, py - 6, px, py + 6);
|
g2.drawLine(px, py - 6, px, py + 6);
|
||||||
g2.setPaintMode();
|
g2.setPaintMode();
|
||||||
@@ -1241,13 +1237,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||||
g2.setPaintMode();
|
g2.setPaintMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Light Pen mode indicator
|
|
||||||
if (lightPenMode) {
|
|
||||||
g2.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
|
|
||||||
g2.setColor(new Color(50, 255, 50));
|
|
||||||
g2.drawString("LP", ox + 2, oy + rows * cellHeight + 14);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||||
@@ -1267,8 +1256,6 @@ public class TerminalPanel extends JPanel {
|
|||||||
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
|
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
|
||||||
if (bg >= 0xf0 && bg <= 0xff) {
|
if (bg >= 0xf0 && bg <= 0xff) {
|
||||||
int idx = bg - 0xf0;
|
int idx = bg - 0xf0;
|
||||||
// Neutral black (0xf0) and black (0xf8) should use window bgColor
|
|
||||||
// to avoid visible seams between field bg and window bg
|
|
||||||
if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) {
|
if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) {
|
||||||
return bgColor;
|
return bgColor;
|
||||||
}
|
}
|
||||||
@@ -1288,22 +1275,26 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (blinkTimer != null)
|
if (blinkTimer != null)
|
||||||
blinkTimer.stop();
|
blinkTimer.stop();
|
||||||
}
|
}
|
||||||
private Runnable onLightPenToggle;
|
|
||||||
|
|
||||||
public void setOnLightPenToggle(Runnable callback) {
|
|
||||||
this.onLightPenToggle = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void toggleLightPen() {
|
public void toggleLightPen() {
|
||||||
this.lightPenMode = !this.lightPenMode;
|
this.lightPenMode = !this.lightPenMode;
|
||||||
System.out.println("Light Pen mode: " + (this.lightPenMode ? "ON" : "OFF"));
|
|
||||||
if (onLightPenToggle != null) {
|
|
||||||
onLightPenToggle.run();
|
|
||||||
}
|
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLightPenMode() {
|
public boolean isLightPenMode() {
|
||||||
return this.lightPenMode;
|
return this.lightPenMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int print(Graphics g, java.awt.print.PageFormat pageFormat, int pageIndex) {
|
||||||
|
if (pageIndex > 0) return NO_SUCH_PAGE;
|
||||||
|
Graphics2D g2d = (Graphics2D) g;
|
||||||
|
g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
|
||||||
|
double scaleX = pageFormat.getImageableWidth() / (double) Math.max(1, getWidth());
|
||||||
|
double scaleY = pageFormat.getImageableHeight() / (double) Math.max(1, getHeight());
|
||||||
|
double scale = Math.min(scaleX, scaleY);
|
||||||
|
g2d.scale(scale, scale);
|
||||||
|
paint(g2d);
|
||||||
|
return PAGE_EXISTS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class ScreenExporterTest {
|
||||||
|
|
||||||
|
private Telnet3270Client client;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
|
||||||
|
client = new Telnet3270Client(config);
|
||||||
|
client.getInputProcessor().setKeyboardLocked(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlainTextExport() throws IOException {
|
||||||
|
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
|
||||||
|
|
||||||
|
File tempTxt = File.createTempFile("export_test_", ".txt");
|
||||||
|
tempTxt.deleteOnExit();
|
||||||
|
|
||||||
|
ScreenExporter.exportToText(client, tempTxt);
|
||||||
|
|
||||||
|
assertTrue(tempTxt.exists());
|
||||||
|
assertTrue(tempTxt.length() > 0);
|
||||||
|
|
||||||
|
String content = Files.readString(tempTxt.toPath());
|
||||||
|
assertTrue(content.contains("TSO/E LOGON SCREEN - WELCOME"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHtmlExport() throws IOException {
|
||||||
|
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
|
||||||
|
|
||||||
|
File tempHtml = File.createTempFile("export_test_", ".html");
|
||||||
|
tempHtml.deleteOnExit();
|
||||||
|
|
||||||
|
ScreenExporter.exportToHtml(client, tempHtml);
|
||||||
|
|
||||||
|
assertTrue(tempHtml.exists());
|
||||||
|
assertTrue(tempHtml.length() > 0);
|
||||||
|
|
||||||
|
String html = Files.readString(tempHtml.toPath());
|
||||||
|
assertTrue(html.contains("<!DOCTYPE html>"));
|
||||||
|
assertTrue(html.contains("TSO/E LOGON SCREEN - WELCOME"));
|
||||||
|
assertTrue(html.contains("font-family: 'Courier New', Courier, monospace"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class StatusBarTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStatusBarDoesNotContainLightPenButton() {
|
||||||
|
StatusBar statusBar = new StatusBar();
|
||||||
|
|
||||||
|
// Ensure no JButton exists in StatusBar components (lightpen button removed)
|
||||||
|
for (Component comp : statusBar.getComponents()) {
|
||||||
|
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStatusBarUpdatesWithClient() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
|
||||||
|
config.setLuName("TSU001");
|
||||||
|
config.setCodePage("1047");
|
||||||
|
Telnet3270Client client = new Telnet3270Client(config);
|
||||||
|
|
||||||
|
StatusBar statusBar = new StatusBar();
|
||||||
|
statusBar.setClient(client, null);
|
||||||
|
statusBar.updateStatus();
|
||||||
|
|
||||||
|
// Check labels
|
||||||
|
boolean foundCodePage = false;
|
||||||
|
boolean foundModel = false;
|
||||||
|
for (Component comp : statusBar.getComponents()) {
|
||||||
|
if (comp instanceof JLabel) {
|
||||||
|
JLabel label = (JLabel) comp;
|
||||||
|
if ("CP1047".equals(label.getText())) {
|
||||||
|
foundCodePage = true;
|
||||||
|
}
|
||||||
|
if (label.getText() != null && label.getText().contains("3279-4")) {
|
||||||
|
foundModel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(foundCodePage, "Should display CP1047");
|
||||||
|
assertTrue(foundModel, "Should display IBM-3279-4 model info");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ public class ConnectionConfig {
|
|||||||
private int dynamicCols = 80;
|
private int dynamicCols = 80;
|
||||||
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||||
private String codePage = "037";
|
private String codePage = "037";
|
||||||
|
private String associatedPrinterLu = null;
|
||||||
|
|
||||||
public ConnectionConfig() {}
|
public ConnectionConfig() {}
|
||||||
|
|
||||||
@@ -214,4 +215,20 @@ public class ConnectionConfig {
|
|||||||
}
|
}
|
||||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
|
||||||
|
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
|
||||||
|
|
||||||
|
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
|
||||||
|
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
|
||||||
|
pcfg.setUseTls(useTls);
|
||||||
|
pcfg.setTlsVerifyCert(tlsVerifyCert);
|
||||||
|
pcfg.setCertificateVerifier(certificateVerifier);
|
||||||
|
pcfg.setSslProtocol(sslProtocol);
|
||||||
|
pcfg.setConnectTimeoutMs(connectTimeoutMs);
|
||||||
|
pcfg.setPrinterLuName(associatedPrinterLu);
|
||||||
|
pcfg.setAssociatedDisplayLuName(luName);
|
||||||
|
pcfg.setCodePage(codePage);
|
||||||
|
return pcfg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,4 +158,43 @@ public class EbcdicTranslator {
|
|||||||
public static List<CodePage> getAllCodePages() {
|
public static List<CodePage> getAllCodePages() {
|
||||||
return CodePageRegistry.getAvailableCodePages();
|
return CodePageRegistry.getAvailableCodePages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||||
|
*/
|
||||||
|
public char getAplGraphic(int ec) {
|
||||||
|
switch (ec & 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' -> '│'
|
||||||
|
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 ebcdicToUnicode(ec & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,16 @@ public class DataStreamProcessor {
|
|||||||
this.ftDft = ftDft;
|
this.ftDft = ftDft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private haus.nightmare.lib3270j.printer.PrintSCS3270 embeddedScsProcessor;
|
||||||
|
|
||||||
|
public void setEmbeddedScsProcessor(haus.nightmare.lib3270j.printer.PrintSCS3270 scsProcessor) {
|
||||||
|
this.embeddedScsProcessor = scsProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public haus.nightmare.lib3270j.printer.PrintSCS3270 getEmbeddedScsProcessor() {
|
||||||
|
return embeddedScsProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
|
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
|
||||||
this.inputProcessor = inputProcessor;
|
this.inputProcessor = inputProcessor;
|
||||||
}
|
}
|
||||||
@@ -1168,6 +1178,9 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
public void processSCSData(byte[] data, int offset, int length) {
|
public void processSCSData(byte[] data, int offset, int length) {
|
||||||
log.info("Received embedded SCS printer data (" + length + " bytes)");
|
log.info("Received embedded SCS printer data (" + length + " bytes)");
|
||||||
|
if (embeddedScsProcessor != null && data != null && length > 0) {
|
||||||
|
embeddedScsProcessor.processHostData(data, offset, length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
|
public void setScreenToBindSize(int primaryRows, int primaryCols, int altRows, int altCols, int bindFlags) {
|
||||||
|
|||||||
@@ -478,6 +478,10 @@ public class QueryReplyBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildAuxDevice() {
|
||||||
|
return buildAuxDev();
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] buildAuxDev() {
|
private byte[] buildAuxDev() {
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x00, 0x09, 0x00, 0x07,
|
0x00, 0x09, 0x00, 0x07,
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print Output Destination & Spool Interface (PD3270).
|
||||||
|
* Implements 1:1 functional compatibility with IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PD3270.
|
||||||
|
*
|
||||||
|
* Supports printing to:
|
||||||
|
* 1. In-memory buffer / capture (for programmatic access or UI display)
|
||||||
|
* 2. Local disk file (with append/overwrite modes)
|
||||||
|
* 3. External system print command / pipe process (e.g. lpr, lp)
|
||||||
|
*/
|
||||||
|
public class PD3270 {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(PD3270.class.getName());
|
||||||
|
|
||||||
|
private final PrinterConfig config;
|
||||||
|
private String destination;
|
||||||
|
private boolean open = false;
|
||||||
|
|
||||||
|
private OutputStream targetOutputStream;
|
||||||
|
private Writer targetWriter;
|
||||||
|
private Process pipeProcess;
|
||||||
|
private final ByteArrayOutputStream memoryStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
private long byteCount = 0;
|
||||||
|
private int pageCount = 0;
|
||||||
|
private Charset outputCharset = StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
public PD3270() {
|
||||||
|
this(new PrinterConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PD3270(PrinterConfig config) {
|
||||||
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.destination = this.config.getDestinationTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open print destination matching IBM HoD openPrinter specification.
|
||||||
|
* @param destination Target path, command string, or null/empty for in-memory capture.
|
||||||
|
* @return true if opened successfully, false on error.
|
||||||
|
*/
|
||||||
|
public synchronized boolean openPrinter(String destination) {
|
||||||
|
if (open) {
|
||||||
|
closePrinter();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.destination = destination != null ? destination : (config != null ? config.getDestinationTarget() : null);
|
||||||
|
this.byteCount = 0;
|
||||||
|
this.pageCount = 0;
|
||||||
|
this.memoryStream.reset();
|
||||||
|
|
||||||
|
PrinterConfig.DestinationType destType = config != null ? config.getDestinationType() : PrinterConfig.DestinationType.MEMORY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.destination != null && !this.destination.trim().isEmpty()) {
|
||||||
|
String trimmedDest = this.destination.trim();
|
||||||
|
if (destType == PrinterConfig.DestinationType.COMMAND || trimmedDest.startsWith("|")) {
|
||||||
|
String cmd = trimmedDest.startsWith("|") ? trimmedDest.substring(1).trim() : trimmedDest;
|
||||||
|
log.info("Opening printer pipe to process: " + cmd);
|
||||||
|
pipeProcess = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
|
||||||
|
targetOutputStream = new BufferedOutputStream(pipeProcess.getOutputStream());
|
||||||
|
targetWriter = new OutputStreamWriter(targetOutputStream, outputCharset);
|
||||||
|
} else if (destType == PrinterConfig.DestinationType.FILE || destType != PrinterConfig.DestinationType.MEMORY) {
|
||||||
|
log.info("Opening printer file: " + trimmedDest);
|
||||||
|
File file = new File(trimmedDest);
|
||||||
|
if (file.getParentFile() != null) {
|
||||||
|
file.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
targetOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
|
||||||
|
targetWriter = new OutputStreamWriter(targetOutputStream, outputCharset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open = true;
|
||||||
|
log.fine("PD3270 printer opened: destination=" + this.destination);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.SEVERE, "Failed to open printer destination: " + destination, e);
|
||||||
|
abortPrinter();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write raw bytes directly to print output destination.
|
||||||
|
*/
|
||||||
|
public synchronized void writePrintBytes(byte[] data, int offset, int length) {
|
||||||
|
if (!open) {
|
||||||
|
openPrinter(destination);
|
||||||
|
}
|
||||||
|
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
memoryStream.write(data, offset, length);
|
||||||
|
byteCount += length;
|
||||||
|
|
||||||
|
if (targetOutputStream != null) {
|
||||||
|
targetOutputStream.write(data, offset, length);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.log(Level.WARNING, "Error writing bytes to printer destination", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a single character to the active print destination.
|
||||||
|
*/
|
||||||
|
public synchronized void writePrintChar(char c) {
|
||||||
|
if (!open) {
|
||||||
|
openPrinter(destination);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] b = String.valueOf(c).getBytes(outputCharset);
|
||||||
|
memoryStream.write(b);
|
||||||
|
byteCount += b.length;
|
||||||
|
|
||||||
|
if (targetWriter != null) {
|
||||||
|
targetWriter.write(c);
|
||||||
|
} else if (targetOutputStream != null) {
|
||||||
|
targetOutputStream.write(b);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.log(Level.WARNING, "Error writing character to printer", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a string to the active print destination.
|
||||||
|
*/
|
||||||
|
public synchronized void writePrintString(String s) {
|
||||||
|
if (s == null || s.isEmpty()) return;
|
||||||
|
if (!open) {
|
||||||
|
openPrinter(destination);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] b = s.getBytes(outputCharset);
|
||||||
|
memoryStream.write(b);
|
||||||
|
byteCount += b.length;
|
||||||
|
|
||||||
|
if (targetWriter != null) {
|
||||||
|
targetWriter.write(s);
|
||||||
|
} else if (targetOutputStream != null) {
|
||||||
|
targetOutputStream.write(b);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.log(Level.WARNING, "Error writing string to printer", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a line with system line-separator.
|
||||||
|
*/
|
||||||
|
public synchronized void writePrintLine(String line) {
|
||||||
|
writePrintString((line != null ? line : "") + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance / Form Feed to next page.
|
||||||
|
*/
|
||||||
|
public synchronized void formFeed() {
|
||||||
|
pageCount++;
|
||||||
|
writePrintChar('\f');
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush all buffered print data to target stream.
|
||||||
|
*/
|
||||||
|
public synchronized void flush() {
|
||||||
|
try {
|
||||||
|
if (targetWriter != null) {
|
||||||
|
targetWriter.flush();
|
||||||
|
}
|
||||||
|
if (targetOutputStream != null) {
|
||||||
|
targetOutputStream.flush();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.log(Level.FINE, "Exception flushing print stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the printer destination cleanly.
|
||||||
|
*/
|
||||||
|
public synchronized void closePrinter() {
|
||||||
|
if (!open) return;
|
||||||
|
try {
|
||||||
|
flush();
|
||||||
|
if (targetWriter != null) {
|
||||||
|
targetWriter.close();
|
||||||
|
targetWriter = null;
|
||||||
|
}
|
||||||
|
if (targetOutputStream != null) {
|
||||||
|
targetOutputStream.close();
|
||||||
|
targetOutputStream = null;
|
||||||
|
}
|
||||||
|
if (pipeProcess != null) {
|
||||||
|
pipeProcess.getOutputStream().close();
|
||||||
|
try {
|
||||||
|
pipeProcess.waitFor();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
pipeProcess = null;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.log(Level.FINE, "Exception closing printer", e);
|
||||||
|
} finally {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort the printer and clean up resources immediately.
|
||||||
|
*/
|
||||||
|
public synchronized void abortPrinter() {
|
||||||
|
try {
|
||||||
|
if (targetWriter != null) {
|
||||||
|
try { targetWriter.close(); } catch (Exception ignored) {}
|
||||||
|
targetWriter = null;
|
||||||
|
}
|
||||||
|
if (targetOutputStream != null) {
|
||||||
|
try { targetOutputStream.close(); } catch (Exception ignored) {}
|
||||||
|
targetOutputStream = null;
|
||||||
|
}
|
||||||
|
if (pipeProcess != null) {
|
||||||
|
pipeProcess.destroy();
|
||||||
|
pipeProcess = null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Status & Accessors ==========
|
||||||
|
|
||||||
|
public boolean isOpen() { return open; }
|
||||||
|
|
||||||
|
public String getDestination() { return destination; }
|
||||||
|
|
||||||
|
public long getByteCount() { return byteCount; }
|
||||||
|
|
||||||
|
public int getPageCount() { return pageCount; }
|
||||||
|
|
||||||
|
public synchronized String getCapturedText() {
|
||||||
|
return new String(memoryStream.toByteArray(), outputCharset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized byte[] getCapturedBytes() {
|
||||||
|
return memoryStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void resetCapture() {
|
||||||
|
memoryStream.reset();
|
||||||
|
byteCount = 0;
|
||||||
|
pageCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Charset getOutputCharset() { return outputCharset; }
|
||||||
|
|
||||||
|
public void setOutputCharset(Charset charset) {
|
||||||
|
if (charset != null) {
|
||||||
|
this.outputCharset = charset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LU Type 3 3270 Printer Data Stream Processor (PrintPS3270 / DS3270P).
|
||||||
|
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintPS3270 / DS3270P.
|
||||||
|
*
|
||||||
|
* Simulates a 3270 printer buffer, processes Write Control Characters (WCC),
|
||||||
|
* interprets 3270 buffer orders (SBA, SF, SFE, SA, RA, EUA, GE, NL, EM, FF, CR),
|
||||||
|
* and renders formatted lines to PD3270.
|
||||||
|
*/
|
||||||
|
public class PrintPS3270 {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(PrintPS3270.class.getName());
|
||||||
|
|
||||||
|
private final PrinterConfig config;
|
||||||
|
private final PD3270 pd;
|
||||||
|
private final EbcdicTranslator translator;
|
||||||
|
|
||||||
|
private int rows = 24;
|
||||||
|
private int cols = 80;
|
||||||
|
private int bufferSize = 24 * 80;
|
||||||
|
private int bufferAddress = 0;
|
||||||
|
|
||||||
|
// Buffer planes
|
||||||
|
private byte[] textPlane;
|
||||||
|
private byte[] attrPlane;
|
||||||
|
private byte[] colorPlane;
|
||||||
|
private byte[] hilitePlane;
|
||||||
|
|
||||||
|
private int currentPrintFormat = PrinterConstants.PRINT_FMT_80_COL;
|
||||||
|
|
||||||
|
public PrintPS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
|
||||||
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.pd = pd != null ? pd : new PD3270(this.config);
|
||||||
|
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
|
||||||
|
|
||||||
|
int mpp = this.config.getMpp();
|
||||||
|
this.cols = (mpp > 0) ? mpp : 80;
|
||||||
|
this.rows = (this.config.getMpl() > 0) ? this.config.getMpl() : 24;
|
||||||
|
this.bufferSize = this.rows * this.cols;
|
||||||
|
|
||||||
|
initBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initBuffer() {
|
||||||
|
this.textPlane = new byte[bufferSize];
|
||||||
|
this.attrPlane = new byte[bufferSize];
|
||||||
|
this.colorPlane = new byte[bufferSize];
|
||||||
|
this.hilitePlane = new byte[bufferSize];
|
||||||
|
erasePrintBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase all contents of the printer buffer to nulls (0x00).
|
||||||
|
*/
|
||||||
|
public synchronized void erasePrintBuffer() {
|
||||||
|
Arrays.fill(textPlane, (byte) 0x00);
|
||||||
|
Arrays.fill(attrPlane, (byte) 0x00);
|
||||||
|
Arrays.fill(colorPlane, (byte) 0x00);
|
||||||
|
Arrays.fill(hilitePlane, (byte) 0x00);
|
||||||
|
this.bufferAddress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for processing LU Type 3 3270 Printer Data Stream records.
|
||||||
|
*/
|
||||||
|
public synchronized void process3270PrintDS(byte[] data, int offset, int length) {
|
||||||
|
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmd = data[offset] & 0xFF;
|
||||||
|
switch (cmd) {
|
||||||
|
case DS3270Constants.CMD_WRITE:
|
||||||
|
case DS3270Constants.SNA_CMD_W:
|
||||||
|
processWrite(data, offset, length, false);
|
||||||
|
break;
|
||||||
|
case DS3270Constants.CMD_ERASE_WRITE:
|
||||||
|
case DS3270Constants.SNA_CMD_EW:
|
||||||
|
processEraseWrite(data, offset, length);
|
||||||
|
break;
|
||||||
|
case DS3270Constants.CMD_ERASE_WRITE_ALT:
|
||||||
|
case DS3270Constants.SNA_CMD_EWA:
|
||||||
|
processEraseWriteAlternate(data, offset, length);
|
||||||
|
break;
|
||||||
|
case DS3270Constants.CMD_ERASE_ALL_UNPROTECTED:
|
||||||
|
case DS3270Constants.SNA_CMD_EAU:
|
||||||
|
erasePrintBuffer();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// If command byte is unrecognized, treat entire stream as write without erase
|
||||||
|
processWrite(data, offset - 1, length + 1, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processWrite(byte[] data, int offset, int length, boolean eraseFirst) {
|
||||||
|
if (eraseFirst) {
|
||||||
|
erasePrintBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = offset + 1; // Skip command byte
|
||||||
|
int end = offset + length;
|
||||||
|
|
||||||
|
if (idx >= end) return;
|
||||||
|
|
||||||
|
// WCC (Write Control Character)
|
||||||
|
int wcc = data[idx++] & 0xFF;
|
||||||
|
boolean startPrint = isStartPrint(wcc);
|
||||||
|
this.currentPrintFormat = getPrintFormat(wcc);
|
||||||
|
|
||||||
|
byte currentColor = 0;
|
||||||
|
byte currentHilite = 0;
|
||||||
|
|
||||||
|
while (idx < end) {
|
||||||
|
int b = data[idx++] & 0xFF;
|
||||||
|
|
||||||
|
switch (b) {
|
||||||
|
case PrinterConstants.ORDER_SBA:
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int b1 = data[idx++] & 0xFF;
|
||||||
|
int b2 = data[idx++] & 0xFF;
|
||||||
|
bufferAddress = decodeBufferAddress(b1, b2) % bufferSize;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_SF:
|
||||||
|
if (idx < end) {
|
||||||
|
byte fa = data[idx++];
|
||||||
|
setCellFA(bufferAddress, fa);
|
||||||
|
bufferAddress = (bufferAddress + 1) % bufferSize;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_SFE:
|
||||||
|
if (idx < end) {
|
||||||
|
int pairCount = data[idx++] & 0xFF;
|
||||||
|
byte fa = 0;
|
||||||
|
for (int p = 0; p < pairCount && idx + 1 < end; p++) {
|
||||||
|
int type = data[idx++] & 0xFF;
|
||||||
|
int val = data[idx++] & 0xFF;
|
||||||
|
if (type == 0x00 || type == 0xC0) {
|
||||||
|
fa = (byte) val;
|
||||||
|
} else if (type == PrinterConstants.SA_COLOR) {
|
||||||
|
currentColor = (byte) val;
|
||||||
|
} else if (type == PrinterConstants.SA_HILITE) {
|
||||||
|
currentHilite = (byte) val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCellFA(bufferAddress, fa);
|
||||||
|
colorPlane[bufferAddress] = currentColor;
|
||||||
|
hilitePlane[bufferAddress] = currentHilite;
|
||||||
|
bufferAddress = (bufferAddress + 1) % bufferSize;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_SA:
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int type = data[idx++] & 0xFF;
|
||||||
|
int val = data[idx++] & 0xFF;
|
||||||
|
if (type == PrinterConstants.SA_COLOR) {
|
||||||
|
currentColor = (byte) val;
|
||||||
|
} else if (type == PrinterConstants.SA_HILITE) {
|
||||||
|
currentHilite = (byte) val;
|
||||||
|
} else if (type == PrinterConstants.SA_RESET) {
|
||||||
|
currentColor = 0;
|
||||||
|
currentHilite = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_RA:
|
||||||
|
if (idx + 2 < end) {
|
||||||
|
int b1 = data[idx++] & 0xFF;
|
||||||
|
int b2 = data[idx++] & 0xFF;
|
||||||
|
int stopAddr = decodeBufferAddress(b1, b2) % bufferSize;
|
||||||
|
byte fillChar = data[idx++];
|
||||||
|
if (stopAddr == bufferAddress) {
|
||||||
|
Arrays.fill(textPlane, fillChar);
|
||||||
|
} else {
|
||||||
|
while (bufferAddress != stopAddr) {
|
||||||
|
textPlane[bufferAddress] = fillChar;
|
||||||
|
colorPlane[bufferAddress] = currentColor;
|
||||||
|
hilitePlane[bufferAddress] = currentHilite;
|
||||||
|
bufferAddress = (bufferAddress + 1) % bufferSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_EUA:
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int b1 = data[idx++] & 0xFF;
|
||||||
|
int b2 = data[idx++] & 0xFF;
|
||||||
|
int stopAddr = decodeBufferAddress(b1, b2) % bufferSize;
|
||||||
|
while (bufferAddress != stopAddr) {
|
||||||
|
textPlane[bufferAddress] = 0x00;
|
||||||
|
bufferAddress = (bufferAddress + 1) % bufferSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_IC:
|
||||||
|
case PrinterConstants.ORDER_PT:
|
||||||
|
// Position cursor / tab
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_GE:
|
||||||
|
if (idx < end) {
|
||||||
|
byte geByte = data[idx++];
|
||||||
|
textPlane[bufferAddress] = geByte;
|
||||||
|
colorPlane[bufferAddress] = currentColor;
|
||||||
|
hilitePlane[bufferAddress] = currentHilite;
|
||||||
|
bufferAddress = (bufferAddress + 1) % bufferSize;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_NL:
|
||||||
|
// Advance bufferAddress to start of next line
|
||||||
|
int curRow = bufferAddress / cols;
|
||||||
|
bufferAddress = ((curRow + 1) * cols) % bufferSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_EM:
|
||||||
|
// End of message: trigger print buffer flush
|
||||||
|
flushPrintBuffer();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_FF:
|
||||||
|
pd.formFeed();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PrinterConstants.ORDER_CR:
|
||||||
|
bufferAddress = (bufferAddress / cols) * cols;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Standard printable EBCDIC character
|
||||||
|
textPlane[bufferAddress] = (byte) b;
|
||||||
|
colorPlane[bufferAddress] = currentColor;
|
||||||
|
hilitePlane[bufferAddress] = currentHilite;
|
||||||
|
bufferAddress = (bufferAddress + 1) % bufferSize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPrint) {
|
||||||
|
flushPrintBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processEraseWrite(byte[] data, int offset, int length) {
|
||||||
|
processWrite(data, offset, length, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processEraseWriteAlternate(byte[] data, int offset, int length) {
|
||||||
|
processWrite(data, offset, length, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCellFA(int addr, byte fa) {
|
||||||
|
attrPlane[addr] = fa;
|
||||||
|
textPlane[addr] = 0x00; // Field attributes display as blanks/nulls
|
||||||
|
}
|
||||||
|
|
||||||
|
private int decodeBufferAddress(int b1, int b2) {
|
||||||
|
// Fast 3270 12-bit / 14-bit address calculation
|
||||||
|
if ((b1 & 0xC0) == 0) {
|
||||||
|
return ((b1 & 0x3F) << 8) | b2;
|
||||||
|
}
|
||||||
|
return ((b1 & 0x3F) << 6) | (b2 & 0x3F);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format and print a specific row from the presentation buffer.
|
||||||
|
*/
|
||||||
|
public synchronized void printLine(int row, int length) {
|
||||||
|
if (row < 0 || row >= rows) return;
|
||||||
|
int lineLen = Math.min(length > 0 ? length : cols, cols);
|
||||||
|
int startAddr = row * cols;
|
||||||
|
|
||||||
|
char[] chars = new char[lineLen];
|
||||||
|
for (int c = 0; c < lineLen; c++) {
|
||||||
|
int ebc = textPlane[startAddr + c] & 0xFF;
|
||||||
|
if (ebc == 0x00) {
|
||||||
|
chars[c] = ' ';
|
||||||
|
} else {
|
||||||
|
chars[c] = translator.ebcdicToUnicode(ebc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim trailing blanks
|
||||||
|
int end = lineLen;
|
||||||
|
while (end > 0 && chars[end - 1] == ' ') {
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end > 0) {
|
||||||
|
pd.writePrintString(new String(chars, 0, end));
|
||||||
|
}
|
||||||
|
pd.writePrintString("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print direct raw EBCDIC text through translator.
|
||||||
|
*/
|
||||||
|
public synchronized void printText(byte[] text, int len) {
|
||||||
|
if (text == null || len <= 0) return;
|
||||||
|
int actualLen = Math.min(len, text.length);
|
||||||
|
char[] chars = new char[actualLen];
|
||||||
|
for (int i = 0; i < actualLen; i++) {
|
||||||
|
int ebc = text[i] & 0xFF;
|
||||||
|
chars[i] = (ebc == 0x00) ? ' ' : translator.ebcdicToUnicode(ebc);
|
||||||
|
}
|
||||||
|
pd.writePrintString(new String(chars));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the entire 3270 printer buffer according to current formatting parameters.
|
||||||
|
*/
|
||||||
|
public synchronized void formatBufferToPrint(int lineLength) {
|
||||||
|
int effLineLen = lineLength > 0 ? lineLength : cols;
|
||||||
|
for (int r = 0; r < rows; r++) {
|
||||||
|
printLine(r, effLineLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush current printer buffer to PD3270 destination.
|
||||||
|
*/
|
||||||
|
public synchronized void flushPrintBuffer() {
|
||||||
|
int lineLen = cols;
|
||||||
|
switch (currentPrintFormat) {
|
||||||
|
case PrinterConstants.PRINT_FMT_40_COL: lineLen = 40; break;
|
||||||
|
case PrinterConstants.PRINT_FMT_64_COL: lineLen = 64; break;
|
||||||
|
case PrinterConstants.PRINT_FMT_80_COL: lineLen = 80; break;
|
||||||
|
default: lineLen = cols; break;
|
||||||
|
}
|
||||||
|
formatBufferToPrint(lineLen);
|
||||||
|
pd.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Attribute & WCC Calculation Helpers ==========
|
||||||
|
|
||||||
|
public boolean isStartPrint(int wcc) {
|
||||||
|
return (wcc & PrinterConstants.WCC_START_PRINT_BIT) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPrintFormat(int wcc) {
|
||||||
|
return wcc & PrinterConstants.WCC_PRINT_FORMAT_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int calculateColor(int colorAttr) {
|
||||||
|
// Map IBM 3270 color attribute code (0xF1=Blue, 0xF2=Red, 0xF3=Pink, 0xF4=Green, 0xF5=Turquoise, 0xF6=Yellow, 0xF7=White)
|
||||||
|
switch (colorAttr) {
|
||||||
|
case 0xF1: return 1; // Blue
|
||||||
|
case 0xF2: return 2; // Red
|
||||||
|
case 0xF3: return 3; // Pink
|
||||||
|
case 0xF4: return 4; // Green
|
||||||
|
case 0xF5: return 5; // Turquoise
|
||||||
|
case 0xF6: return 6; // Yellow
|
||||||
|
case 0xF7: return 7; // Neutral White
|
||||||
|
default: return 0; // Default Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int calculateHighlight(int hiliteAttr) {
|
||||||
|
switch (hiliteAttr) {
|
||||||
|
case PrinterConstants.SEAC_BLINK: return 1;
|
||||||
|
case PrinterConstants.SEAC_REVERSE: return 2;
|
||||||
|
case PrinterConstants.SEAC_UNDERLINE: return 4;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int calculateCharset(int csAttr) {
|
||||||
|
return csAttr & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Accessors ==========
|
||||||
|
|
||||||
|
public int getRows() { return rows; }
|
||||||
|
public int getCols() { return cols; }
|
||||||
|
public int getBufferSize() { return bufferSize; }
|
||||||
|
public int getBufferAddress() { return bufferAddress; }
|
||||||
|
public PD3270 getPD() { return pd; }
|
||||||
|
public EbcdicTranslator getTranslator() { return translator; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SCS (SNA Character String) Interpreter for IBM LU Type 1 Printer Sessions.
|
||||||
|
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.PrintSCS3270.
|
||||||
|
*/
|
||||||
|
public class PrintSCS3270 {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(PrintSCS3270.class.getName());
|
||||||
|
|
||||||
|
private final PrinterConfig config;
|
||||||
|
private final PD3270 pd;
|
||||||
|
private final EbcdicTranslator translator;
|
||||||
|
|
||||||
|
// Formatting Parameters
|
||||||
|
private int mpp = PrinterConstants.DEFAULT_MPP; // Maximum Presentation Position (Line Length)
|
||||||
|
private int mpl = PrinterConstants.DEFAULT_MPL; // Maximum Page Length
|
||||||
|
private int leftMargin = 1;
|
||||||
|
private int rightMargin = PrinterConstants.DEFAULT_MPP;
|
||||||
|
private int topMargin = 1;
|
||||||
|
private int bottomMargin = PrinterConstants.DEFAULT_MPL;
|
||||||
|
private int cpi = PrinterConstants.DEFAULT_CPI;
|
||||||
|
private int lpi = PrinterConstants.DEFAULT_LPI;
|
||||||
|
|
||||||
|
// Tab Stops (1-based arrays)
|
||||||
|
private int[] horizontalTabs = new int[0];
|
||||||
|
private int[] verticalTabs = new int[0];
|
||||||
|
|
||||||
|
// Current State
|
||||||
|
private int currentRow = 1;
|
||||||
|
private int currentCol = 1;
|
||||||
|
private boolean doubleWidth = false;
|
||||||
|
private int activeColor = 0;
|
||||||
|
private int activeHighlight = PrinterConstants.SEAC_DEFAULT;
|
||||||
|
private int textOrientation = 0;
|
||||||
|
private boolean presentationEnabled = true;
|
||||||
|
|
||||||
|
// Line buffering for print composition
|
||||||
|
private char[] lineBuffer;
|
||||||
|
private boolean lineModified = false;
|
||||||
|
|
||||||
|
public PrintSCS3270(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
|
||||||
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.pd = pd != null ? pd : new PD3270(this.config);
|
||||||
|
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
|
||||||
|
|
||||||
|
resetSCSFormatDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all SCS formatting parameters and presentation state to initial defaults.
|
||||||
|
*/
|
||||||
|
public synchronized void resetSCSFormatDefaults() {
|
||||||
|
this.mpp = config.getMpp();
|
||||||
|
this.mpl = config.getMpl();
|
||||||
|
this.leftMargin = config.getLeftMargin();
|
||||||
|
this.rightMargin = config.getRightMargin();
|
||||||
|
this.topMargin = config.getTopMargin();
|
||||||
|
this.bottomMargin = config.getBottomMargin();
|
||||||
|
this.cpi = config.getCpi();
|
||||||
|
this.lpi = config.getLpi();
|
||||||
|
|
||||||
|
this.horizontalTabs = new int[0];
|
||||||
|
this.verticalTabs = new int[0];
|
||||||
|
|
||||||
|
this.currentRow = this.topMargin;
|
||||||
|
this.currentCol = this.leftMargin;
|
||||||
|
this.doubleWidth = false;
|
||||||
|
this.activeColor = 0;
|
||||||
|
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
|
||||||
|
this.textOrientation = 0;
|
||||||
|
this.presentationEnabled = true;
|
||||||
|
|
||||||
|
this.lineBuffer = new char[Math.max(256, mpp + 1)];
|
||||||
|
Arrays.fill(lineBuffer, ' ');
|
||||||
|
this.lineModified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process host data stream containing SCS orders and characters.
|
||||||
|
* @param data Byte array from host.
|
||||||
|
* @param offset Starting offset.
|
||||||
|
* @param length Number of bytes to process.
|
||||||
|
*/
|
||||||
|
public synchronized void processHostData(byte[] data, int offset, int length) {
|
||||||
|
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = offset;
|
||||||
|
int end = offset + length;
|
||||||
|
|
||||||
|
while (idx < end) {
|
||||||
|
int b = data[idx] & 0xFF;
|
||||||
|
|
||||||
|
// Check 2-byte SCS prefix (0x2B)
|
||||||
|
if (b == PrinterConstants.SCS_PREFIX_2B) {
|
||||||
|
if (idx + 1 >= end) {
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int subOrder = data[idx + 1] & 0xFF;
|
||||||
|
int paramLen = (idx + 2 < end) ? (data[idx + 2] & 0xFF) : 0;
|
||||||
|
int orderTotalLen = 2 + (paramLen > 0 ? (paramLen + 1) : 1); // 0x2B + SubOrder + paramLen + payload
|
||||||
|
|
||||||
|
// In standard SCS, paramLen byte specifies length of following parameters
|
||||||
|
// or paramLen is total length including length byte.
|
||||||
|
int bytesAvailable = end - idx;
|
||||||
|
int sliceLen = Math.min(orderTotalLen, bytesAvailable);
|
||||||
|
|
||||||
|
switch (subOrder) {
|
||||||
|
case PrinterConstants.SCS_SHF:
|
||||||
|
processSetHorizontalFormat(data, idx, sliceLen);
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_SVF:
|
||||||
|
processSetVerticalFormat(data, idx, sliceLen);
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_SLD:
|
||||||
|
processSetLineDensity(data, idx, sliceLen);
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_STO:
|
||||||
|
processSetTextOrientation(data, idx, sliceLen);
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_SEAC:
|
||||||
|
processSetEnhancedAttribute(data, idx, sliceLen);
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_PPA:
|
||||||
|
processPresentationPositionAdvancing(data, idx, sliceLen);
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_PPV:
|
||||||
|
processPresentationPositionVertical(data, idx, sliceLen);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.fine("Unrecognized 0x2B SCS sub-order: 0x" + Integer.toHexString(subOrder));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx += sliceLen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check SA (Set Attribute 0x28)
|
||||||
|
if (b == PrinterConstants.SCS_SA) {
|
||||||
|
if (idx + 2 < end) {
|
||||||
|
processSetAttribute(data, idx, 3);
|
||||||
|
idx += 3;
|
||||||
|
} else {
|
||||||
|
idx = end;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check TRS (Transparent Stream 0x35)
|
||||||
|
if (b == PrinterConstants.SCS_TRS) {
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int trsLen = data[idx + 1] & 0xFF;
|
||||||
|
int actualTrs = Math.min(trsLen, end - (idx + 2));
|
||||||
|
processTransparentStream(data, idx + 2, actualTrs);
|
||||||
|
idx += 2 + actualTrs;
|
||||||
|
} else {
|
||||||
|
idx = end;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Single-Byte SCS Controls
|
||||||
|
switch (b) {
|
||||||
|
case PrinterConstants.SCS_NUL:
|
||||||
|
// Null - ignored
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_CR:
|
||||||
|
carriageReturn();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_LF:
|
||||||
|
lineFeed();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_NL:
|
||||||
|
case PrinterConstants.SCS_RNLS:
|
||||||
|
newLine();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_FF:
|
||||||
|
formFeed();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_BS:
|
||||||
|
case PrinterConstants.SCS_NBS:
|
||||||
|
backspace();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_HT:
|
||||||
|
processHorizontalTab();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_VT:
|
||||||
|
processVerticalTab();
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_ENP:
|
||||||
|
presentationEnabled = true;
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_INP:
|
||||||
|
presentationEnabled = false;
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_BEL:
|
||||||
|
// Sound alarm
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_GE:
|
||||||
|
if (idx + 1 < end) {
|
||||||
|
int geChar = data[idx + 1] & 0xFF;
|
||||||
|
processGraphicEscapeChar(geChar);
|
||||||
|
idx += 2;
|
||||||
|
} else {
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PrinterConstants.SCS_SP:
|
||||||
|
case PrinterConstants.SCS_RSP:
|
||||||
|
printCharacter(' ');
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Standard printable character
|
||||||
|
if (presentationEnabled) {
|
||||||
|
char ch = translator.ebcdicToUnicode(b);
|
||||||
|
printCharacter(ch);
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printCharacter(char c) {
|
||||||
|
if (currentCol > rightMargin || currentCol > mpp) {
|
||||||
|
advanceToNextLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLineBufferSize(currentCol + (doubleWidth ? 2 : 1));
|
||||||
|
lineBuffer[currentCol - 1] = c;
|
||||||
|
lineModified = true;
|
||||||
|
|
||||||
|
currentCol += (doubleWidth ? 2 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processGraphicEscapeChar(int ec) {
|
||||||
|
char unicode = translator.getAplGraphic(ec);
|
||||||
|
if (unicode == 0 || unicode == ' ') {
|
||||||
|
unicode = translator.ebcdicToUnicode(ec);
|
||||||
|
}
|
||||||
|
printCharacter(unicode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureLineBufferSize(int requiredSize) {
|
||||||
|
if (requiredSize > lineBuffer.length) {
|
||||||
|
int newSize = Math.max(requiredSize + 32, lineBuffer.length * 2);
|
||||||
|
char[] newBuf = new char[newSize];
|
||||||
|
Arrays.fill(newBuf, ' ');
|
||||||
|
System.arraycopy(lineBuffer, 0, newBuf, 0, lineBuffer.length);
|
||||||
|
lineBuffer = newBuf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush the current line buffer to PD3270.
|
||||||
|
*/
|
||||||
|
public synchronized void flushLineBuffer() {
|
||||||
|
if (lineModified) {
|
||||||
|
// Trim trailing spaces
|
||||||
|
int len = lineBuffer.length;
|
||||||
|
while (len > 0 && lineBuffer[len - 1] == ' ') {
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
|
pd.writePrintString(new String(lineBuffer, 0, len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pd.writePrintString("\n");
|
||||||
|
Arrays.fill(lineBuffer, ' ');
|
||||||
|
lineModified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== SCS Order Implementations ==========
|
||||||
|
|
||||||
|
public synchronized void carriageReturn() {
|
||||||
|
currentCol = leftMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void lineFeed() {
|
||||||
|
flushLineBuffer();
|
||||||
|
currentRow++;
|
||||||
|
if (currentRow > bottomMargin || currentRow > mpl) {
|
||||||
|
advanceToNextPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void newLine() {
|
||||||
|
carriageReturn();
|
||||||
|
lineFeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void formFeed() {
|
||||||
|
flushLineBuffer();
|
||||||
|
pd.formFeed();
|
||||||
|
currentRow = topMargin;
|
||||||
|
currentCol = leftMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void backspace() {
|
||||||
|
if (currentCol > leftMargin) {
|
||||||
|
currentCol -= (doubleWidth ? 2 : 1);
|
||||||
|
if (currentCol < leftMargin) currentCol = leftMargin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void advanceToNextLine() {
|
||||||
|
newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void advanceToNextPage() {
|
||||||
|
formFeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Tab Stops and Calculations ==========
|
||||||
|
|
||||||
|
public synchronized int calculateHorizontalTab(int currentPos) {
|
||||||
|
if (horizontalTabs != null && horizontalTabs.length > 0) {
|
||||||
|
for (int tab : horizontalTabs) {
|
||||||
|
if (tab > currentPos) {
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default tab stop: advance to next multiple of 8 + 1
|
||||||
|
int nextTab = ((currentPos / 8) + 1) * 8 + 1;
|
||||||
|
return Math.min(nextTab, rightMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int calculateVerticalTab(int currentLine) {
|
||||||
|
if (verticalTabs != null && verticalTabs.length > 0) {
|
||||||
|
for (int tab : verticalTabs) {
|
||||||
|
if (tab > currentLine) {
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1; // No more vertical tabs on this page
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processHorizontalTab() {
|
||||||
|
int nextTab = calculateHorizontalTab(currentCol);
|
||||||
|
if (nextTab <= rightMargin) {
|
||||||
|
currentCol = nextTab;
|
||||||
|
} else {
|
||||||
|
advanceToNextLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processVerticalTab() {
|
||||||
|
int nextTab = calculateVerticalTab(currentRow);
|
||||||
|
if (nextTab > 0 && nextTab <= bottomMargin) {
|
||||||
|
while (currentRow < nextTab) {
|
||||||
|
lineFeed();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
advanceToNextPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Multi-Byte Order Parsers ==========
|
||||||
|
|
||||||
|
public synchronized void setHorizontalFormat(int lineLength, int[] tabs) {
|
||||||
|
this.mpp = lineLength > 0 ? lineLength : PrinterConstants.DEFAULT_MPP;
|
||||||
|
if (this.rightMargin > this.mpp) {
|
||||||
|
this.rightMargin = this.mpp;
|
||||||
|
}
|
||||||
|
if (tabs != null) {
|
||||||
|
this.horizontalTabs = Arrays.copyOf(tabs, tabs.length);
|
||||||
|
Arrays.sort(this.horizontalTabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setVerticalFormat(int pageLength, int[] tabs) {
|
||||||
|
this.mpl = pageLength > 0 ? pageLength : PrinterConstants.DEFAULT_MPL;
|
||||||
|
if (this.bottomMargin > this.mpl) {
|
||||||
|
this.bottomMargin = this.mpl;
|
||||||
|
}
|
||||||
|
if (tabs != null) {
|
||||||
|
this.verticalTabs = Arrays.copyOf(tabs, tabs.length);
|
||||||
|
Arrays.sort(this.verticalTabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setHorizontalMargins(int leftMargin, int rightMargin) {
|
||||||
|
this.leftMargin = Math.max(1, leftMargin);
|
||||||
|
this.rightMargin = Math.min(this.mpp, Math.max(this.leftMargin, rightMargin));
|
||||||
|
if (currentCol < this.leftMargin) currentCol = this.leftMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setVerticalMargins(int topMargin, int bottomMargin) {
|
||||||
|
this.topMargin = Math.max(1, topMargin);
|
||||||
|
this.bottomMargin = Math.min(this.mpl, Math.max(this.topMargin, bottomMargin));
|
||||||
|
if (currentRow < this.topMargin) currentRow = this.topMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setPrintDensity(int cpi, int lpi) {
|
||||||
|
if (cpi > 0) this.cpi = cpi;
|
||||||
|
if (lpi > 0) this.lpi = lpi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setEnhancedHighlight(int highlightType) {
|
||||||
|
this.activeHighlight = highlightType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void startDoubleWidthCharacters() {
|
||||||
|
this.doubleWidth = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void endDoubleWidthCharacters() {
|
||||||
|
this.doubleWidth = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processSetHorizontalFormat(byte[] data, int offset, int len) {
|
||||||
|
if (len < 4) return;
|
||||||
|
int mppVal = data[offset + 3] & 0xFF;
|
||||||
|
if (mppVal > 0) {
|
||||||
|
this.mpp = mppVal;
|
||||||
|
this.rightMargin = Math.min(this.rightMargin, this.mpp);
|
||||||
|
}
|
||||||
|
if (len >= 6) {
|
||||||
|
int lm = data[offset + 4] & 0xFF;
|
||||||
|
int rm = data[offset + 5] & 0xFF;
|
||||||
|
if (lm > 0) this.leftMargin = lm;
|
||||||
|
if (rm > 0 && rm >= lm) this.rightMargin = rm;
|
||||||
|
}
|
||||||
|
if (len > 6) {
|
||||||
|
int tabCount = len - 6;
|
||||||
|
int[] tabs = new int[tabCount];
|
||||||
|
for (int i = 0; i < tabCount; i++) {
|
||||||
|
tabs[i] = data[offset + 6 + i] & 0xFF;
|
||||||
|
}
|
||||||
|
Arrays.sort(tabs);
|
||||||
|
this.horizontalTabs = tabs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processSetVerticalFormat(byte[] data, int offset, int len) {
|
||||||
|
if (len < 4) return;
|
||||||
|
int mplVal = data[offset + 3] & 0xFF;
|
||||||
|
if (mplVal > 0) {
|
||||||
|
this.mpl = mplVal;
|
||||||
|
this.bottomMargin = Math.min(this.bottomMargin, this.mpl);
|
||||||
|
}
|
||||||
|
if (len >= 6) {
|
||||||
|
int tm = data[offset + 4] & 0xFF;
|
||||||
|
int bm = data[offset + 5] & 0xFF;
|
||||||
|
if (tm > 0) this.topMargin = tm;
|
||||||
|
if (bm > 0 && bm >= tm) this.bottomMargin = bm;
|
||||||
|
}
|
||||||
|
if (len > 6) {
|
||||||
|
int tabCount = len - 6;
|
||||||
|
int[] tabs = new int[tabCount];
|
||||||
|
for (int i = 0; i < tabCount; i++) {
|
||||||
|
tabs[i] = data[offset + 6 + i] & 0xFF;
|
||||||
|
}
|
||||||
|
Arrays.sort(tabs);
|
||||||
|
this.verticalTabs = tabs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processSetLineDensity(byte[] data, int offset, int len) {
|
||||||
|
if (len < 4) return;
|
||||||
|
int points = data[offset + 3] & 0xFF;
|
||||||
|
if (points > 0) {
|
||||||
|
// Line density in points / inch (72 points = 1 inch)
|
||||||
|
// 12 points = 6 LPI, 9 points = 8 LPI, 18 points = 4 LPI
|
||||||
|
this.lpi = Math.max(1, 72 / points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processSetTextOrientation(byte[] data, int offset, int len) {
|
||||||
|
if (len >= 4) {
|
||||||
|
this.textOrientation = data[offset + 3] & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processSetEnhancedAttribute(byte[] data, int offset, int len) {
|
||||||
|
if (len >= 5) {
|
||||||
|
int attrVal = data[offset + 4] & 0xFF;
|
||||||
|
setEnhancedHighlight(attrVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processPresentationPositionAdvancing(byte[] data, int offset, int len) {
|
||||||
|
if (len < 5) return;
|
||||||
|
int subfn = data[offset + 3] & 0xFF;
|
||||||
|
int val = data[offset + 4] & 0xFF;
|
||||||
|
|
||||||
|
if (subfn == PrinterConstants.POS_ABSOLUTE) {
|
||||||
|
currentCol = Math.min(mpp, Math.max(leftMargin, val));
|
||||||
|
} else if (subfn == PrinterConstants.POS_RELATIVE) {
|
||||||
|
currentCol = Math.min(mpp, currentCol + val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processPresentationPositionVertical(byte[] data, int offset, int len) {
|
||||||
|
if (len < 5) return;
|
||||||
|
int subfn = data[offset + 3] & 0xFF;
|
||||||
|
int val = data[offset + 4] & 0xFF;
|
||||||
|
|
||||||
|
if (subfn == PrinterConstants.POS_ABSOLUTE) {
|
||||||
|
while (currentRow < val && currentRow < bottomMargin) {
|
||||||
|
lineFeed();
|
||||||
|
}
|
||||||
|
} else if (subfn == PrinterConstants.POS_RELATIVE) {
|
||||||
|
for (int i = 0; i < val && currentRow < bottomMargin; i++) {
|
||||||
|
lineFeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processSetAttribute(byte[] data, int offset, int len) {
|
||||||
|
if (len < 3) return;
|
||||||
|
int attrType = data[offset + 1] & 0xFF;
|
||||||
|
int attrVal = data[offset + 2] & 0xFF;
|
||||||
|
|
||||||
|
if (attrType == PrinterConstants.SA_COLOR) {
|
||||||
|
this.activeColor = attrVal;
|
||||||
|
} else if (attrType == PrinterConstants.SA_HILITE) {
|
||||||
|
this.activeHighlight = attrVal;
|
||||||
|
} else if (attrType == PrinterConstants.SA_RESET) {
|
||||||
|
this.activeColor = 0;
|
||||||
|
this.activeHighlight = PrinterConstants.SEAC_DEFAULT;
|
||||||
|
this.doubleWidth = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processTransparentStream(byte[] data, int offset, int len) {
|
||||||
|
if (len > 0) {
|
||||||
|
pd.writePrintBytes(data, offset, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Accessors ==========
|
||||||
|
|
||||||
|
public int getLineLength() { return mpp; }
|
||||||
|
public void setLineLength(int len) { this.mpp = len; }
|
||||||
|
|
||||||
|
public int getPageLength() { return mpl; }
|
||||||
|
public void setPageLength(int len) { this.mpl = len; }
|
||||||
|
|
||||||
|
public int getLeftMargin() { return leftMargin; }
|
||||||
|
public int getRightMargin() { return rightMargin; }
|
||||||
|
public int getTopMargin() { return topMargin; }
|
||||||
|
public int getBottomMargin() { return bottomMargin; }
|
||||||
|
|
||||||
|
public int getCurrentRow() { return currentRow; }
|
||||||
|
public int getCurrentColumn() { return currentCol; }
|
||||||
|
|
||||||
|
public int getLinesPerInch() { return lpi; }
|
||||||
|
public int getCharsPerInch() { return cpi; }
|
||||||
|
|
||||||
|
public boolean isDoubleWidth() { return doubleWidth; }
|
||||||
|
public void setDoubleWidth(boolean dw) { this.doubleWidth = dw; }
|
||||||
|
|
||||||
|
public int getActiveColor() { return activeColor; }
|
||||||
|
public void setActiveColor(int color) { this.activeColor = color; }
|
||||||
|
|
||||||
|
public int getActiveHighlight() { return activeHighlight; }
|
||||||
|
public void setActiveHighlight(int hilite) { this.activeHighlight = hilite; }
|
||||||
|
|
||||||
|
public int getTextOrientation() { return textOrientation; }
|
||||||
|
|
||||||
|
public PD3270 getPD() { return pd; }
|
||||||
|
public EbcdicTranslator getTranslator() { return translator; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import java.util.EventObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event object representing state and progress changes in a 3270/3287 printer session.
|
||||||
|
*/
|
||||||
|
public class PrintSessionEvent extends EventObject {
|
||||||
|
|
||||||
|
public static final int EVENT_JOB_STARTED = 1;
|
||||||
|
public static final int EVENT_JOB_DATA = 2;
|
||||||
|
public static final int EVENT_PAGE_COMPLETE = 3;
|
||||||
|
public static final int EVENT_JOB_COMPLETE = 4;
|
||||||
|
public static final int EVENT_STATUS_CHANGED = 5;
|
||||||
|
public static final int EVENT_ERROR = 6;
|
||||||
|
|
||||||
|
private final int eventType;
|
||||||
|
private final String jobName;
|
||||||
|
private final int pageNumber;
|
||||||
|
private final long byteCount;
|
||||||
|
private final int totalPages;
|
||||||
|
private final int statusCode;
|
||||||
|
private final String message;
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
public PrintSessionEvent(Object source, int eventType, String jobName, int pageNumber,
|
||||||
|
long byteCount, int totalPages, int statusCode, String message, byte[] data) {
|
||||||
|
super(source);
|
||||||
|
this.eventType = eventType;
|
||||||
|
this.jobName = jobName;
|
||||||
|
this.pageNumber = pageNumber;
|
||||||
|
this.byteCount = byteCount;
|
||||||
|
this.totalPages = totalPages;
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEventType() { return eventType; }
|
||||||
|
public String getJobName() { return jobName; }
|
||||||
|
public int getPageNumber() { return pageNumber; }
|
||||||
|
public long getByteCount() { return byteCount; }
|
||||||
|
public int getTotalPages() { return totalPages; }
|
||||||
|
public int getStatusCode() { return statusCode; }
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public byte[] getData() { return data; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PrintSessionEvent[type=" + eventType + ", page=" + pageNumber + ", bytes=" + byteCount +
|
||||||
|
", status=" + statusCode + ", msg=" + message + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import java.util.EventListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface for 3270/3287 printer session events.
|
||||||
|
*/
|
||||||
|
public interface PrintSessionListener extends EventListener {
|
||||||
|
|
||||||
|
/** Fired when a new print job begins. */
|
||||||
|
void onPrintJobStarted(PrintSessionEvent event);
|
||||||
|
|
||||||
|
/** Fired when raw or formatted print data is processed. */
|
||||||
|
void onPrintJobData(PrintSessionEvent event);
|
||||||
|
|
||||||
|
/** Fired when a page is completed (Form Feed). */
|
||||||
|
void onPrintJobPageComplete(PrintSessionEvent event);
|
||||||
|
|
||||||
|
/** Fired when the print job is completed (End Of Job). */
|
||||||
|
void onPrintJobComplete(PrintSessionEvent event);
|
||||||
|
|
||||||
|
/** Fired when the printer session status changes. */
|
||||||
|
void onPrinterStatusChanged(PrintSessionEvent event);
|
||||||
|
|
||||||
|
/** Fired when a printer error occurs. */
|
||||||
|
void onPrinterError(PrintSessionEvent event);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.tls.TlsCertificateVerifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration parameters for an IBM 3287 / 3286 printer session.
|
||||||
|
*/
|
||||||
|
public class PrinterConfig {
|
||||||
|
|
||||||
|
public enum DestinationType {
|
||||||
|
MEMORY,
|
||||||
|
FILE,
|
||||||
|
COMMAND,
|
||||||
|
LISTENER
|
||||||
|
}
|
||||||
|
|
||||||
|
private String host;
|
||||||
|
private int port = 23;
|
||||||
|
private boolean useTls = false;
|
||||||
|
private boolean tlsVerifyCert = true;
|
||||||
|
private TlsCertificateVerifier certificateVerifier = null;
|
||||||
|
private String sslProtocol = "TLS";
|
||||||
|
private int connectTimeoutMs = 15000;
|
||||||
|
private int soTimeoutMs = 0;
|
||||||
|
private boolean tcpNoDelay = true;
|
||||||
|
private boolean soKeepAlive = true;
|
||||||
|
|
||||||
|
// LU and Association
|
||||||
|
private String printerLuName = null;
|
||||||
|
private String associatedDisplayLuName = null;
|
||||||
|
private String deviceType = PrinterConstants.DEV_IBM_3287_1;
|
||||||
|
private String codePage = "037";
|
||||||
|
|
||||||
|
// Geometry and formatting
|
||||||
|
private int mpp = PrinterConstants.DEFAULT_MPP;
|
||||||
|
private int mpl = PrinterConstants.DEFAULT_MPL;
|
||||||
|
private int leftMargin = 1;
|
||||||
|
private int rightMargin = PrinterConstants.DEFAULT_MPP;
|
||||||
|
private int topMargin = 1;
|
||||||
|
private int bottomMargin = PrinterConstants.DEFAULT_MPL;
|
||||||
|
private int cpi = PrinterConstants.DEFAULT_CPI;
|
||||||
|
private int lpi = PrinterConstants.DEFAULT_LPI;
|
||||||
|
|
||||||
|
// Spool destination
|
||||||
|
private DestinationType destinationType = DestinationType.MEMORY;
|
||||||
|
private String destinationTarget = null;
|
||||||
|
private boolean formFeedAtEoj = true;
|
||||||
|
private boolean autoFlushOnEoj = true;
|
||||||
|
private boolean autoReconnect = false;
|
||||||
|
|
||||||
|
public PrinterConfig() {}
|
||||||
|
|
||||||
|
public PrinterConfig(String host, int port) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrinterConfig(String host, int port, String printerLuName) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.printerLuName = printerLuName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrinterConfig(String host, int port, String printerLuName, boolean useTls) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.printerLuName = printerLuName;
|
||||||
|
this.useTls = useTls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getHost() { return host; }
|
||||||
|
public void setHost(String host) { this.host = host; }
|
||||||
|
|
||||||
|
public int getPort() { return port; }
|
||||||
|
public void setPort(int port) { this.port = port; }
|
||||||
|
|
||||||
|
public boolean isUseTls() { return useTls; }
|
||||||
|
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||||
|
|
||||||
|
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||||
|
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
|
||||||
|
|
||||||
|
public TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||||
|
public void setCertificateVerifier(TlsCertificateVerifier certificateVerifier) { this.certificateVerifier = certificateVerifier; }
|
||||||
|
|
||||||
|
public String getSslProtocol() { return sslProtocol; }
|
||||||
|
public void setSslProtocol(String sslProtocol) { this.sslProtocol = sslProtocol; }
|
||||||
|
|
||||||
|
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
||||||
|
public void setConnectTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; }
|
||||||
|
|
||||||
|
public int getSoTimeoutMs() { return soTimeoutMs; }
|
||||||
|
public void setSoTimeoutMs(int soTimeoutMs) { this.soTimeoutMs = soTimeoutMs; }
|
||||||
|
|
||||||
|
public boolean isTcpNoDelay() { return tcpNoDelay; }
|
||||||
|
public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; }
|
||||||
|
|
||||||
|
public boolean isSoKeepAlive() { return soKeepAlive; }
|
||||||
|
public void setSoKeepAlive(boolean soKeepAlive) { this.soKeepAlive = soKeepAlive; }
|
||||||
|
|
||||||
|
public String getPrinterLuName() { return printerLuName; }
|
||||||
|
public void setPrinterLuName(String printerLuName) { this.printerLuName = printerLuName; }
|
||||||
|
|
||||||
|
public String getAssociatedDisplayLuName() { return associatedDisplayLuName; }
|
||||||
|
public void setAssociatedDisplayLuName(String associatedDisplayLuName) { this.associatedDisplayLuName = associatedDisplayLuName; }
|
||||||
|
|
||||||
|
public String getDeviceType() { return deviceType; }
|
||||||
|
public void setDeviceType(String deviceType) {
|
||||||
|
this.deviceType = (deviceType != null && !deviceType.trim().isEmpty()) ? deviceType.trim() : PrinterConstants.DEV_IBM_3287_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCodePage() { return codePage; }
|
||||||
|
public void setCodePage(String codePage) {
|
||||||
|
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMpp() { return mpp; }
|
||||||
|
public void setMpp(int mpp) {
|
||||||
|
this.mpp = mpp > 0 ? mpp : PrinterConstants.DEFAULT_MPP;
|
||||||
|
if (this.rightMargin > this.mpp) this.rightMargin = this.mpp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMpl() { return mpl; }
|
||||||
|
public void setMpl(int mpl) {
|
||||||
|
this.mpl = mpl > 0 ? mpl : PrinterConstants.DEFAULT_MPL;
|
||||||
|
if (this.bottomMargin > this.mpl) this.bottomMargin = this.mpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLeftMargin() { return leftMargin; }
|
||||||
|
public void setLeftMargin(int leftMargin) { this.leftMargin = Math.max(1, leftMargin); }
|
||||||
|
|
||||||
|
public int getRightMargin() { return rightMargin; }
|
||||||
|
public void setRightMargin(int rightMargin) { this.rightMargin = Math.min(mpp, Math.max(leftMargin, rightMargin)); }
|
||||||
|
|
||||||
|
public int getTopMargin() { return topMargin; }
|
||||||
|
public void setTopMargin(int topMargin) { this.topMargin = Math.max(1, topMargin); }
|
||||||
|
|
||||||
|
public int getBottomMargin() { return bottomMargin; }
|
||||||
|
public void setBottomMargin(int bottomMargin) { this.bottomMargin = Math.min(mpl, Math.max(topMargin, bottomMargin)); }
|
||||||
|
|
||||||
|
public int getCpi() { return cpi; }
|
||||||
|
public void setCpi(int cpi) { this.cpi = cpi > 0 ? cpi : PrinterConstants.DEFAULT_CPI; }
|
||||||
|
|
||||||
|
public int getLpi() { return lpi; }
|
||||||
|
public void setLpi(int lpi) { this.lpi = lpi > 0 ? lpi : PrinterConstants.DEFAULT_LPI; }
|
||||||
|
|
||||||
|
public DestinationType getDestinationType() { return destinationType; }
|
||||||
|
public void setDestinationType(DestinationType destinationType) { this.destinationType = destinationType != null ? destinationType : DestinationType.MEMORY; }
|
||||||
|
|
||||||
|
public String getDestinationTarget() { return destinationTarget; }
|
||||||
|
public void setDestinationTarget(String destinationTarget) { this.destinationTarget = destinationTarget; }
|
||||||
|
|
||||||
|
public boolean isFormFeedAtEoj() { return formFeedAtEoj; }
|
||||||
|
public void setFormFeedAtEoj(boolean formFeedAtEoj) { this.formFeedAtEoj = formFeedAtEoj; }
|
||||||
|
|
||||||
|
public boolean isAutoFlushOnEoj() { return autoFlushOnEoj; }
|
||||||
|
public void setAutoFlushOnEoj(boolean autoFlushOnEoj) { this.autoFlushOnEoj = autoFlushOnEoj; }
|
||||||
|
|
||||||
|
public boolean isAutoReconnect() { return autoReconnect; }
|
||||||
|
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for IBM 3287 / 3286 Printer Sessions and SCS (SNA Character String) Protocol.
|
||||||
|
* Conforms to IBM Host On-Demand v14 (com.ibm.eNetwork.ECL.tn3270p.*) and RFC 2355.
|
||||||
|
*/
|
||||||
|
public final class PrinterConstants {
|
||||||
|
|
||||||
|
private PrinterConstants() {}
|
||||||
|
|
||||||
|
// ========== LU BIND Types ==========
|
||||||
|
public static final short LU_TYPE_UNKNOWN = 0;
|
||||||
|
public static final short LU_TYPE_1_SCS = 1;
|
||||||
|
public static final short LU_TYPE_3_DS = 3;
|
||||||
|
|
||||||
|
// ========== Device Type Names ==========
|
||||||
|
public static final String DEV_IBM_3287_1 = "IBM-3287-1";
|
||||||
|
public static final String DEV_IBM_3287_2 = "IBM-3287-2";
|
||||||
|
public static final String DEV_IBM_3286_2 = "IBM-3286-2";
|
||||||
|
public static final String DEV_IBM_387_1 = "IBM-387-1";
|
||||||
|
public static final String DEV_IBM_387_2 = "IBM-387-2";
|
||||||
|
|
||||||
|
// ========== SCS Single-Byte Control Codes (EBCDIC) ==========
|
||||||
|
public static final int SCS_NUL = 0x00; // Null
|
||||||
|
public static final int SCS_HT = 0x05; // Horizontal Tab
|
||||||
|
public static final int SCS_RNLS = 0x06; // Required New Line
|
||||||
|
public static final int SCS_RCR = 0x07; // Required Carriage Return
|
||||||
|
public static final int SCS_GE = 0x08; // Graphic Escape
|
||||||
|
public static final int SCS_VT = 0x0B; // Vertical Tab
|
||||||
|
public static final int SCS_FF = 0x0C; // Form Feed
|
||||||
|
public static final int SCS_CR = 0x0D; // Carriage Return
|
||||||
|
public static final int SCS_ENP = 0x14; // Enable Presentation
|
||||||
|
public static final int SCS_NL = 0x15; // New Line
|
||||||
|
public static final int SCS_BS = 0x16; // Backspace
|
||||||
|
public static final int SCS_POC = 0x17; // Program Operator Communication
|
||||||
|
public static final int SCS_INP = 0x24; // Inhibit Presentation
|
||||||
|
public static final int SCS_LF = 0x25; // Line Feed
|
||||||
|
public static final int SCS_BEL = 0x2F; // Bell / Sound Alarm
|
||||||
|
public static final int SCS_TRS = 0x35; // Transparent Stream (0x35 <len> <bytes>)
|
||||||
|
public static final int SCS_NBS = 0x36; // Numeric Backspace
|
||||||
|
public static final int SCS_SP = 0x40; // Space
|
||||||
|
public static final int SCS_RSP = 0x41; // Required Space
|
||||||
|
|
||||||
|
// ========== SCS Multi-Byte Order Prefixes ==========
|
||||||
|
public static final int SCS_PREFIX_2B = 0x2B; // 2-byte SCS prefix
|
||||||
|
public static final int SCS_SA = 0x28; // Set Attribute (0x28 <type> <val>)
|
||||||
|
|
||||||
|
// 0x2B Sub-orders
|
||||||
|
public static final int SCS_SHF = 0xD1; // Set Horizontal Format
|
||||||
|
public static final int SCS_SVF = 0xD2; // Set Vertical Format
|
||||||
|
public static final int SCS_STO = 0xD3; // Set Text Orientation
|
||||||
|
public static final int SCS_SCS = 0xD4; // Select Character Set
|
||||||
|
public static final int SCS_SEAC = 0xD5; // Set Enhanced Attribute / Highlight
|
||||||
|
public static final int SCS_SLD = 0xD6; // Set Line Density
|
||||||
|
public static final int SCS_PPV = 0xC4; // Presentation Position Vertical
|
||||||
|
public static final int SCS_PPA = 0xC6; // Presentation Position Advancing (Horizontal)
|
||||||
|
|
||||||
|
// SA Attribute Types
|
||||||
|
public static final int SA_RESET = 0x00;
|
||||||
|
public static final int SA_HILITE = 0x41;
|
||||||
|
public static final int SA_COLOR = 0x42;
|
||||||
|
public static final int SA_CHARSET = 0x43;
|
||||||
|
|
||||||
|
// SEAC Highlight Values
|
||||||
|
public static final int SEAC_DEFAULT = 0x00;
|
||||||
|
public static final int SEAC_BLINK = 0xF1;
|
||||||
|
public static final int SEAC_REVERSE = 0xF2;
|
||||||
|
public static final int SEAC_UNDERLINE = 0xF4;
|
||||||
|
|
||||||
|
// PPA/PPV Positioning Types
|
||||||
|
public static final int POS_ABSOLUTE = 0x01; // Absolute position (1-based)
|
||||||
|
public static final int POS_RELATIVE = 0x02; // Relative offset
|
||||||
|
|
||||||
|
// ========== 3270 Print Data Stream Constants (LU3) ==========
|
||||||
|
public static final int WCC_START_PRINT_BIT = 0x08; // Start Print in 3270 WCC
|
||||||
|
public static final int WCC_PRINT_FORMAT_MASK = 0x03; // Formatting: 00=unformatted, 01=40 col, 10=64 col, 11=80 col
|
||||||
|
public static final int PRINT_FMT_UNFORMATTED = 0;
|
||||||
|
public static final int PRINT_FMT_40_COL = 1;
|
||||||
|
public static final int PRINT_FMT_64_COL = 2;
|
||||||
|
public static final int PRINT_FMT_80_COL = 3;
|
||||||
|
|
||||||
|
// 3270 Orders in Print Stream
|
||||||
|
public static final int ORDER_SBA = 0x11;
|
||||||
|
public static final int ORDER_SF = 0x1D;
|
||||||
|
public static final int ORDER_SFE = 0x29;
|
||||||
|
public static final int ORDER_SA = 0x28;
|
||||||
|
public static final int ORDER_MF = 0x2C;
|
||||||
|
public static final int ORDER_IC = 0x13;
|
||||||
|
public static final int ORDER_PT = 0x05;
|
||||||
|
public static final int ORDER_RA = 0x3C;
|
||||||
|
public static final int ORDER_EUA = 0x12;
|
||||||
|
public static final int ORDER_GE = 0x08;
|
||||||
|
public static final int ORDER_NL = 0x15;
|
||||||
|
public static final int ORDER_EM = 0x19; // End of Message
|
||||||
|
public static final int ORDER_FF = 0x0C;
|
||||||
|
public static final int ORDER_CR = 0x0D;
|
||||||
|
|
||||||
|
// ========== Printer Session Status Codes ==========
|
||||||
|
public static final int STATUS_CONNECTING = 650;
|
||||||
|
public static final int STATUS_NEGOTIATING = 651;
|
||||||
|
public static final int STATUS_CONNECTED = 652;
|
||||||
|
public static final int STATUS_SECURITY = 654;
|
||||||
|
public static final int STATUS_BIND_ERROR = 655;
|
||||||
|
public static final int STATUS_DISCONNECTED = 656;
|
||||||
|
public static final int STATUS_PRINTER_READY = 700;
|
||||||
|
public static final int STATUS_PRINTING = 701;
|
||||||
|
public static final int STATUS_PAGE_COMPLETE = 702;
|
||||||
|
public static final int STATUS_JOB_COMPLETE = 703;
|
||||||
|
public static final int STATUS_PRINTER_BUSY = 704;
|
||||||
|
public static final int STATUS_PRINTER_ERROR = 705;
|
||||||
|
public static final int STATUS_PRINTER_CLOSED = 706;
|
||||||
|
|
||||||
|
// Default Geometry
|
||||||
|
public static final int DEFAULT_MPP = 80; // Default line length
|
||||||
|
public static final int DEFAULT_MPL = 66; // Default page length (11 inches @ 6 LPI)
|
||||||
|
public static final int DEFAULT_CPI = 10; // 10 chars per inch
|
||||||
|
public static final int DEFAULT_LPI = 6; // 6 lines per inch
|
||||||
|
}
|
||||||
@@ -0,0 +1,765 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||||
|
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
|
||||||
|
import haus.nightmare.lib3270j.tls.TlsTrustManager;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TN3270E Printer Connection & Protocol Engine (Telnet3270EP).
|
||||||
|
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.tn3270p.Telnet3270EP.
|
||||||
|
*/
|
||||||
|
public class Telnet3270EP implements Runnable {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(Telnet3270EP.class.getName());
|
||||||
|
|
||||||
|
private final PrinterConfig config;
|
||||||
|
private final PD3270 pd;
|
||||||
|
private final EbcdicTranslator translator;
|
||||||
|
private final PrintSCS3270 scs;
|
||||||
|
private final PrintPS3270 printPs;
|
||||||
|
|
||||||
|
private final List<PrintSessionListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private Socket socket;
|
||||||
|
private InputStream inputStream;
|
||||||
|
private OutputStream outputStream;
|
||||||
|
private Thread readerThread;
|
||||||
|
private volatile boolean running = false;
|
||||||
|
private volatile boolean connected = false;
|
||||||
|
|
||||||
|
// Protocol State
|
||||||
|
private int statusCode = PrinterConstants.STATUS_DISCONNECTED;
|
||||||
|
private short activeLuType = PrinterConstants.LU_TYPE_UNKNOWN;
|
||||||
|
private String assignedLuName = null;
|
||||||
|
private String negotiatedDeviceType = null;
|
||||||
|
private boolean tn3270eMode = false;
|
||||||
|
private final boolean[] negotiatedFunctions = new boolean[10];
|
||||||
|
|
||||||
|
// Sequence tracking
|
||||||
|
private int sendSeqNumber = 0;
|
||||||
|
private int lastRecvSeqNumber = 0;
|
||||||
|
private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE;
|
||||||
|
|
||||||
|
// Buffer for Telnet streaming
|
||||||
|
private final ByteArrayOutputStream recordBuffer = new ByteArrayOutputStream();
|
||||||
|
private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream();
|
||||||
|
private boolean inSubnegotiation = false;
|
||||||
|
|
||||||
|
public Telnet3270EP(PrinterConfig config) {
|
||||||
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.pd = new PD3270(this.config);
|
||||||
|
this.translator = new EbcdicTranslator(this.config.getCodePage());
|
||||||
|
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
||||||
|
this.printPs = new PrintPS3270(this.config, this.pd, this.translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
|
||||||
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.pd = pd != null ? pd : new PD3270(this.config);
|
||||||
|
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
|
||||||
|
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
||||||
|
this.printPs = new PrintPS3270(this.config, this.pd, this.translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Connection Lifecycle (Fn #1) ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open connection to TN3270E printer host matching open() specification.
|
||||||
|
*/
|
||||||
|
public synchronized boolean open() {
|
||||||
|
if (connected || running) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(PrinterConstants.STATUS_CONNECTING, "Connecting to host " + config.getHost() + ":" + config.getPort());
|
||||||
|
try {
|
||||||
|
if (config.isUseTls()) {
|
||||||
|
SSLContext sslContext = SSLContext.getInstance(config.getSslProtocol() != null ? config.getSslProtocol() : "TLS");
|
||||||
|
TrustManager[] tm = new TrustManager[]{new TlsTrustManager(config.isTlsVerifyCert(), config.getCertificateVerifier())};
|
||||||
|
sslContext.init(null, tm, new SecureRandom());
|
||||||
|
SSLSocketFactory factory = sslContext.getSocketFactory();
|
||||||
|
|
||||||
|
SSLSocket sslSocket = (SSLSocket) factory.createSocket();
|
||||||
|
if (config.getConnectTimeoutMs() > 0) {
|
||||||
|
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()), config.getConnectTimeoutMs());
|
||||||
|
} else {
|
||||||
|
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()));
|
||||||
|
}
|
||||||
|
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||||
|
sslSocket.setKeepAlive(config.isSoKeepAlive());
|
||||||
|
if (config.getSoTimeoutMs() > 0) {
|
||||||
|
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||||
|
}
|
||||||
|
sslSocket.startHandshake();
|
||||||
|
this.socket = sslSocket;
|
||||||
|
} else {
|
||||||
|
Socket sock = new Socket();
|
||||||
|
if (config.getConnectTimeoutMs() > 0) {
|
||||||
|
sock.connect(new InetSocketAddress(config.getHost(), config.getPort()), config.getConnectTimeoutMs());
|
||||||
|
} else {
|
||||||
|
sock.connect(new InetSocketAddress(config.getHost(), config.getPort()));
|
||||||
|
}
|
||||||
|
sock.setTcpNoDelay(config.isTcpNoDelay());
|
||||||
|
sock.setKeepAlive(config.isSoKeepAlive());
|
||||||
|
if (config.getSoTimeoutMs() > 0) {
|
||||||
|
sock.setSoTimeout(config.getSoTimeoutMs());
|
||||||
|
}
|
||||||
|
this.socket = sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputStream = new BufferedInputStream(socket.getInputStream());
|
||||||
|
this.outputStream = new BufferedOutputStream(socket.getOutputStream());
|
||||||
|
this.connected = true;
|
||||||
|
this.running = true;
|
||||||
|
|
||||||
|
reset();
|
||||||
|
updateStatus(PrinterConstants.STATUS_NEGOTIATING, "Connected, negotiating TN3270E");
|
||||||
|
|
||||||
|
readerThread = new Thread(this, "Telnet3270EP-Reader-" + config.getHost());
|
||||||
|
readerThread.setDaemon(true);
|
||||||
|
readerThread.start();
|
||||||
|
|
||||||
|
// Initiate Telnet negotiation: DO TN3270E
|
||||||
|
sendTelnetCommand(TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.SEVERE, "Failed to connect to printer host", e);
|
||||||
|
updateStatus(PrinterConstants.STATUS_PRINTER_ERROR, "Connection failed: " + e.getMessage());
|
||||||
|
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, e.getMessage());
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close printer connection cleanly.
|
||||||
|
*/
|
||||||
|
public synchronized void close() {
|
||||||
|
running = false;
|
||||||
|
connected = false;
|
||||||
|
|
||||||
|
if (socket != null) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
if (readerThread != null) {
|
||||||
|
readerThread.interrupt();
|
||||||
|
readerThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pd.closePrinter();
|
||||||
|
updateStatus(PrinterConstants.STATUS_DISCONNECTED, "Disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset printer session state.
|
||||||
|
*/
|
||||||
|
public synchronized void reset() {
|
||||||
|
this.activeLuType = PrinterConstants.LU_TYPE_UNKNOWN;
|
||||||
|
this.assignedLuName = null;
|
||||||
|
this.negotiatedDeviceType = null;
|
||||||
|
this.tn3270eMode = false;
|
||||||
|
Arrays.fill(negotiatedFunctions, false);
|
||||||
|
this.sendSeqNumber = 0;
|
||||||
|
this.lastRecvSeqNumber = 0;
|
||||||
|
this.recordBuffer.reset();
|
||||||
|
this.subnegBuffer.reset();
|
||||||
|
this.inSubnegotiation = false;
|
||||||
|
|
||||||
|
scs.resetSCSFormatDefaults();
|
||||||
|
printPs.erasePrintBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send raw data stream to host with IAC (0xFF) escaping.
|
||||||
|
*/
|
||||||
|
public synchronized void send(byte[] data, int length) {
|
||||||
|
if (!connected || outputStream == null || data == null || length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
for (int i = 0; i < length && i < data.length; i++) {
|
||||||
|
int b = data[i] & 0xFF;
|
||||||
|
out.write(b);
|
||||||
|
if (b == TelnetConstants.IAC) {
|
||||||
|
out.write(TelnetConstants.IAC); // 0xFF escaping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputStream.write(out.toByteArray());
|
||||||
|
outputStream.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.log(Level.WARNING, "Error sending data to printer host", e);
|
||||||
|
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, "Send error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== BIND, Ready & EOJ Protocol Handlers (Fn #2, #3, #4) ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process SNA BIND image to determine printer LU type (LU1 SCS vs LU3 3270 DS).
|
||||||
|
* @param bindType Inferred or explicit bind type (LU_TYPE_1_SCS or LU_TYPE_3_DS).
|
||||||
|
*/
|
||||||
|
public synchronized void process_bind(short bindType) {
|
||||||
|
this.activeLuType = bindType;
|
||||||
|
log.info("Printer session BIND accepted, LU type = " +
|
||||||
|
(activeLuType == PrinterConstants.LU_TYPE_1_SCS ? "LU-1 (SCS)" : "LU-3 (3270 DS)"));
|
||||||
|
|
||||||
|
updateStatus(PrinterConstants.STATUS_PRINTER_READY, "Printer session bound as " +
|
||||||
|
(activeLuType == PrinterConstants.LU_TYPE_1_SCS ? "LU-1" : "LU-3"));
|
||||||
|
|
||||||
|
sendPrinterReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse SNA BIND image payload and extract LU type and parameters.
|
||||||
|
*/
|
||||||
|
public synchronized void processBindImage(byte[] bindData, int offset, int length) {
|
||||||
|
if (bindData == null || length < 14) {
|
||||||
|
process_bind(PrinterConstants.LU_TYPE_1_SCS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// In SNA BIND request:
|
||||||
|
// Byte 14 (profile byte / secondary LU type):
|
||||||
|
// 0x01 = LU Type 1 (SCS Printer)
|
||||||
|
// 0x03 = LU Type 3 (3270 Printer Data Stream)
|
||||||
|
int profile = bindData[offset + 14] & 0xFF;
|
||||||
|
short inferredLuType = (profile == 0x03) ? PrinterConstants.LU_TYPE_3_DS : PrinterConstants.LU_TYPE_1_SCS;
|
||||||
|
process_bind(inferredLuType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send TN3270E Positive Response indicating printer readiness (DEVICE-END).
|
||||||
|
*/
|
||||||
|
public synchronized void sendPrinterReady() {
|
||||||
|
sendTN3270EResponse(lastRecvSeqNumber, TN3270EConstants.RSF_POSITIVE_RESPONSE, TN3270EConstants.POS_DEVICE_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle End-Of-Job indicator from host.
|
||||||
|
* @param isComplete true if print job is complete.
|
||||||
|
*/
|
||||||
|
public synchronized void sendEOJ(boolean isComplete) {
|
||||||
|
log.info("Received End-Of-Job (EOJ), isComplete=" + isComplete);
|
||||||
|
if (config.isFormFeedAtEoj()) {
|
||||||
|
pd.formFeed();
|
||||||
|
}
|
||||||
|
if (config.isAutoFlushOnEoj()) {
|
||||||
|
pd.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
firePrintJobComplete(pd.getPageCount(), pd.getByteCount());
|
||||||
|
updateStatus(PrinterConstants.STATUS_JOB_COMPLETE, "Print job completed (" + pd.getPageCount() + " pages)");
|
||||||
|
|
||||||
|
// Send EOJ acknowledgment to host if required
|
||||||
|
if (lastResponseRequired == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(lastRecvSeqNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== TN3270E Responses & Packet Construction ==========
|
||||||
|
|
||||||
|
public synchronized void sendTN3270EResponse(int seqNumber, int respType, int respCode) {
|
||||||
|
if (!tn3270eMode || outputStream == null) return;
|
||||||
|
byte[] resp = new byte[TN3270EConstants.EH_SIZE + 1];
|
||||||
|
resp[0] = (byte) TN3270EConstants.DT_RESPONSE;
|
||||||
|
resp[1] = 0; // Request flag
|
||||||
|
resp[2] = (byte) respType;
|
||||||
|
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||||
|
resp[4] = (byte) (seqNumber & 0xFF);
|
||||||
|
resp[5] = (byte) respCode;
|
||||||
|
|
||||||
|
sendTN3270ERecord(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void sendTN3270EPositiveResponse(int seqNumber) {
|
||||||
|
sendTN3270EResponse(seqNumber, TN3270EConstants.RSF_POSITIVE_RESPONSE, TN3270EConstants.POS_DEVICE_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
|
||||||
|
sendTN3270EResponse(seqNumber, TN3270EConstants.RSF_NEGATIVE_RESPONSE, negCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void sendTN3270ERecord(byte[] record) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
for (byte b : record) {
|
||||||
|
int ub = b & 0xFF;
|
||||||
|
out.write(ub);
|
||||||
|
if (ub == TelnetConstants.IAC) {
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
out.write(TelnetConstants.EOR);
|
||||||
|
|
||||||
|
outputStream.write(out.toByteArray());
|
||||||
|
outputStream.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.log(Level.WARNING, "Error sending TN3270E record", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void sendTelnetCommand(int command, int option) throws IOException {
|
||||||
|
outputStream.write(new byte[]{(byte) TelnetConstants.IAC, (byte) command, (byte) option});
|
||||||
|
outputStream.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Telnet & TN3270E Protocol I/O Loop ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
try {
|
||||||
|
while (running && connected) {
|
||||||
|
int read = inputStream.read(buf);
|
||||||
|
if (read < 0) {
|
||||||
|
log.info("Host closed socket connection");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
processIncomingBytes(buf, 0, read);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running) {
|
||||||
|
log.log(Level.WARNING, "Connection exception in printer reader thread", e);
|
||||||
|
firePrinterError(PrinterConstants.STATUS_PRINTER_ERROR, e.getMessage());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processIncomingBytes(byte[] data, int offset, int length) {
|
||||||
|
int idx = offset;
|
||||||
|
int end = offset + length;
|
||||||
|
|
||||||
|
while (idx < end) {
|
||||||
|
int b = data[idx++] & 0xFF;
|
||||||
|
|
||||||
|
if (inSubnegotiation) {
|
||||||
|
if (b == TelnetConstants.IAC) {
|
||||||
|
if (idx < end && (data[idx] & 0xFF) == TelnetConstants.SE) {
|
||||||
|
idx++;
|
||||||
|
inSubnegotiation = false;
|
||||||
|
processSubnegotiation(subnegBuffer.toByteArray());
|
||||||
|
subnegBuffer.reset();
|
||||||
|
} else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) {
|
||||||
|
subnegBuffer.write(TelnetConstants.IAC);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subnegBuffer.write(b);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == TelnetConstants.IAC) {
|
||||||
|
if (idx >= end) break;
|
||||||
|
int cmd = data[idx++] & 0xFF;
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case TelnetConstants.IAC:
|
||||||
|
recordBuffer.write(TelnetConstants.IAC);
|
||||||
|
break;
|
||||||
|
case TelnetConstants.SB:
|
||||||
|
inSubnegotiation = true;
|
||||||
|
subnegBuffer.reset();
|
||||||
|
break;
|
||||||
|
case TelnetConstants.EOR:
|
||||||
|
processCompleteRecord(recordBuffer.toByteArray());
|
||||||
|
recordBuffer.reset();
|
||||||
|
break;
|
||||||
|
case TelnetConstants.DO:
|
||||||
|
if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
|
||||||
|
break;
|
||||||
|
case TelnetConstants.DONT:
|
||||||
|
if (idx < end) handleTelnetDont(data[idx++] & 0xFF);
|
||||||
|
break;
|
||||||
|
case TelnetConstants.WILL:
|
||||||
|
if (idx < end) handleTelnetWill(data[idx++] & 0xFF);
|
||||||
|
break;
|
||||||
|
case TelnetConstants.WONT:
|
||||||
|
if (idx < end) handleTelnetWont(data[idx++] & 0xFF);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recordBuffer.write(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTelnetDo(int option) {
|
||||||
|
try {
|
||||||
|
if (option == TelnetConstants.TELOPT_TN3270E) {
|
||||||
|
sendTelnetCommand(TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
tn3270eMode = true;
|
||||||
|
} else if (option == TelnetConstants.TELOPT_BINARY || option == TelnetConstants.TELOPT_EOR) {
|
||||||
|
sendTelnetCommand(TelnetConstants.WILL, option);
|
||||||
|
} else {
|
||||||
|
sendTelnetCommand(TelnetConstants.WONT, option);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warning("Error responding to Telnet DO: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTelnetDont(int option) {
|
||||||
|
try {
|
||||||
|
sendTelnetCommand(TelnetConstants.WONT, option);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTelnetWill(int option) {
|
||||||
|
try {
|
||||||
|
if (option == TelnetConstants.TELOPT_TN3270E) {
|
||||||
|
sendTelnetCommand(TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
tn3270eMode = true;
|
||||||
|
} else if (option == TelnetConstants.TELOPT_BINARY || option == TelnetConstants.TELOPT_EOR) {
|
||||||
|
sendTelnetCommand(TelnetConstants.DO, option);
|
||||||
|
} else {
|
||||||
|
sendTelnetCommand(TelnetConstants.DONT, option);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warning("Error responding to Telnet WILL: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTelnetWont(int option) {
|
||||||
|
try {
|
||||||
|
sendTelnetCommand(TelnetConstants.DONT, option);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processSubnegotiation(byte[] sb) {
|
||||||
|
if (sb.length == 0) return;
|
||||||
|
int opt = sb[0] & 0xFF;
|
||||||
|
|
||||||
|
if (opt == TelnetConstants.TELOPT_TN3270E && sb.length > 1) {
|
||||||
|
int op = sb[1] & 0xFF;
|
||||||
|
switch (op) {
|
||||||
|
case TN3270EConstants.OP_SEND:
|
||||||
|
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_DEVICE_TYPE) {
|
||||||
|
sendDeviceTypeRequest();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.OP_DEVICE_TYPE:
|
||||||
|
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_IS) {
|
||||||
|
parseDeviceTypeIs(sb);
|
||||||
|
sendFunctionsRequest();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.OP_FUNCTIONS:
|
||||||
|
if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_IS) {
|
||||||
|
parseFunctionsIs(sb);
|
||||||
|
} else if (sb.length > 2 && (sb[2] & 0xFF) == TN3270EConstants.OP_REQUEST) {
|
||||||
|
parseFunctionsRequest(sb);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendDeviceTypeRequest() {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
out.write(TelnetConstants.SB);
|
||||||
|
out.write(TelnetConstants.TELOPT_TN3270E);
|
||||||
|
out.write(TN3270EConstants.OP_DEVICE_TYPE);
|
||||||
|
out.write(TN3270EConstants.OP_REQUEST);
|
||||||
|
|
||||||
|
String devType = config.getDeviceType() != null ? config.getDeviceType() : PrinterConstants.DEV_IBM_3287_1;
|
||||||
|
out.write(devType.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
|
||||||
|
if (config.getAssociatedDisplayLuName() != null && !config.getAssociatedDisplayLuName().trim().isEmpty()) {
|
||||||
|
out.write(TN3270EConstants.OP_ASSOCIATE);
|
||||||
|
out.write(config.getAssociatedDisplayLuName().trim().getBytes(StandardCharsets.US_ASCII));
|
||||||
|
} else if (config.getPrinterLuName() != null && !config.getPrinterLuName().trim().isEmpty()) {
|
||||||
|
out.write(TN3270EConstants.OP_CONNECT);
|
||||||
|
out.write(config.getPrinterLuName().trim().getBytes(StandardCharsets.US_ASCII));
|
||||||
|
}
|
||||||
|
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
out.write(TelnetConstants.SE);
|
||||||
|
|
||||||
|
outputStream.write(out.toByteArray());
|
||||||
|
outputStream.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warning("Error sending TN3270E device type request: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseDeviceTypeIs(byte[] sb) {
|
||||||
|
// SB TN3270E DEVICE-TYPE IS <devtype> [CONNECT|ASSOCIATE <luname>]
|
||||||
|
int idx = 3;
|
||||||
|
ByteArrayOutputStream devOut = new ByteArrayOutputStream();
|
||||||
|
while (idx < sb.length && (sb[idx] & 0xFF) != TN3270EConstants.OP_CONNECT && (sb[idx] & 0xFF) != TN3270EConstants.OP_ASSOCIATE) {
|
||||||
|
devOut.write(sb[idx++]);
|
||||||
|
}
|
||||||
|
negotiatedDeviceType = new String(devOut.toByteArray(), StandardCharsets.US_ASCII).trim();
|
||||||
|
|
||||||
|
if (idx < sb.length) {
|
||||||
|
idx++; // skip CONNECT / ASSOCIATE
|
||||||
|
ByteArrayOutputStream luOut = new ByteArrayOutputStream();
|
||||||
|
while (idx < sb.length) {
|
||||||
|
luOut.write(sb[idx++]);
|
||||||
|
}
|
||||||
|
assignedLuName = new String(luOut.toByteArray(), StandardCharsets.US_ASCII).trim();
|
||||||
|
}
|
||||||
|
log.info("TN3270E device negotiated: " + negotiatedDeviceType + ", LU=" + assignedLuName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendFunctionsRequest() {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
out.write(TelnetConstants.SB);
|
||||||
|
out.write(TelnetConstants.TELOPT_TN3270E);
|
||||||
|
out.write(TN3270EConstants.OP_FUNCTIONS);
|
||||||
|
out.write(TN3270EConstants.OP_REQUEST);
|
||||||
|
|
||||||
|
// Request BIND-IMAGE, RESPONSES, SCS-CTL-CODES
|
||||||
|
out.write(TN3270EConstants.FUNC_BIND_IMAGE);
|
||||||
|
out.write(TN3270EConstants.FUNC_RESPONSES);
|
||||||
|
out.write(TN3270EConstants.FUNC_SCS_CTL_CODES);
|
||||||
|
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
out.write(TelnetConstants.SE);
|
||||||
|
|
||||||
|
outputStream.write(out.toByteArray());
|
||||||
|
outputStream.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warning("Error sending TN3270E functions request: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseFunctionsIs(byte[] sb) {
|
||||||
|
Arrays.fill(negotiatedFunctions, false);
|
||||||
|
for (int i = 3; i < sb.length; i++) {
|
||||||
|
int fn = sb[i] & 0xFF;
|
||||||
|
if (fn < negotiatedFunctions.length) {
|
||||||
|
negotiatedFunctions[fn] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateStatus(PrinterConstants.STATUS_CONNECTED, "Connected and negotiated with host");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseFunctionsRequest(byte[] sb) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
out.write(TelnetConstants.SB);
|
||||||
|
out.write(TelnetConstants.TELOPT_TN3270E);
|
||||||
|
out.write(TN3270EConstants.OP_FUNCTIONS);
|
||||||
|
out.write(TN3270EConstants.OP_IS);
|
||||||
|
|
||||||
|
Arrays.fill(negotiatedFunctions, false);
|
||||||
|
for (int i = 3; i < sb.length; i++) {
|
||||||
|
int fn = sb[i] & 0xFF;
|
||||||
|
if (fn == TN3270EConstants.FUNC_BIND_IMAGE || fn == TN3270EConstants.FUNC_RESPONSES || fn == TN3270EConstants.FUNC_SCS_CTL_CODES) {
|
||||||
|
negotiatedFunctions[fn] = true;
|
||||||
|
out.write(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.write(TelnetConstants.IAC);
|
||||||
|
out.write(TelnetConstants.SE);
|
||||||
|
|
||||||
|
outputStream.write(out.toByteArray());
|
||||||
|
outputStream.flush();
|
||||||
|
updateStatus(PrinterConstants.STATUS_CONNECTED, "Connected and negotiated with host");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warning("Error replying to TN3270E functions request: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== TN3270E Record Dispatching ==========
|
||||||
|
|
||||||
|
private void processCompleteRecord(byte[] record) {
|
||||||
|
if (record == null || record.length == 0) return;
|
||||||
|
|
||||||
|
if (tn3270eMode) {
|
||||||
|
if (record.length < TN3270EConstants.EH_SIZE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int dataType = record[0] & 0xFF;
|
||||||
|
int reqFlag = record[1] & 0xFF;
|
||||||
|
int respFlag = record[2] & 0xFF;
|
||||||
|
int seqNum = ((record[3] & 0xFF) << 8) | (record[4] & 0xFF);
|
||||||
|
|
||||||
|
this.lastRecvSeqNumber = seqNum;
|
||||||
|
this.lastResponseRequired = respFlag;
|
||||||
|
|
||||||
|
firePrintJobData(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
|
||||||
|
|
||||||
|
switch (dataType) {
|
||||||
|
case TN3270EConstants.DT_SCS_DATA:
|
||||||
|
updateStatus(PrinterConstants.STATUS_PRINTING, "Processing SCS print stream");
|
||||||
|
scs.processHostData(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
|
||||||
|
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNum);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.DT_3270_DATA:
|
||||||
|
updateStatus(PrinterConstants.STATUS_PRINTING, "Processing 3270 printer data stream");
|
||||||
|
printPs.process3270PrintDS(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
|
||||||
|
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNum);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.DT_BIND_IMAGE:
|
||||||
|
processBindImage(record, TN3270EConstants.EH_SIZE, record.length - TN3270EConstants.EH_SIZE);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.DT_UNBIND:
|
||||||
|
log.info("Received TN3270E UNBIND");
|
||||||
|
reset();
|
||||||
|
updateStatus(PrinterConstants.STATUS_CONNECTED, "Session unbound");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.DT_PRINT_EOJ:
|
||||||
|
sendEOJ(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.DT_REQUEST:
|
||||||
|
if (negotiatedFunctions[TN3270EConstants.FUNC_RESPONSES] && respFlag == TN3270EConstants.RSF_ALWAYS_RESPONSE) {
|
||||||
|
sendTN3270EPositiveResponse(seqNum);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TN3270EConstants.DT_RESPONSE:
|
||||||
|
log.fine("Received response, seq=" + seqNum);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Fallback to active LU type
|
||||||
|
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
|
||||||
|
printPs.process3270PrintDS(record, 0, record.length);
|
||||||
|
} else {
|
||||||
|
scs.processHostData(record, 0, record.length);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-TN3270E mode
|
||||||
|
firePrintJobData(record, 0, record.length);
|
||||||
|
if (activeLuType == PrinterConstants.LU_TYPE_3_DS) {
|
||||||
|
printPs.process3270PrintDS(record, 0, record.length);
|
||||||
|
} else {
|
||||||
|
scs.processHostData(record, 0, record.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Event Dispatching & Listeners (Fn #51 - #58) ==========
|
||||||
|
|
||||||
|
public void addPrintListener(PrintSessionListener listener) {
|
||||||
|
if (listener != null && !listeners.contains(listener)) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePrintListener(PrintSessionListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void firePrintJobStarted(String jobName) {
|
||||||
|
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_STARTED, jobName,
|
||||||
|
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), statusCode, "Print job started", null);
|
||||||
|
for (PrintSessionListener l : listeners) {
|
||||||
|
try { l.onPrintJobStarted(event); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void firePrintJobData(byte[] data, int offset, int length) {
|
||||||
|
byte[] copy = null;
|
||||||
|
if (data != null && length > 0) {
|
||||||
|
copy = new byte[length];
|
||||||
|
System.arraycopy(data, offset, copy, 0, length);
|
||||||
|
}
|
||||||
|
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_DATA, null,
|
||||||
|
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), statusCode, "Print data", copy);
|
||||||
|
for (PrintSessionListener l : listeners) {
|
||||||
|
try { l.onPrintJobData(event); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void firePrintJobPageComplete(int pageNumber) {
|
||||||
|
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_PAGE_COMPLETE, null,
|
||||||
|
pageNumber, pd.getByteCount(), pageNumber, statusCode, "Page complete", null);
|
||||||
|
for (PrintSessionListener l : listeners) {
|
||||||
|
try { l.onPrintJobPageComplete(event); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void firePrintJobComplete(int totalPages, long totalBytes) {
|
||||||
|
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_JOB_COMPLETE, null,
|
||||||
|
totalPages, totalBytes, totalPages, statusCode, "Print job complete", null);
|
||||||
|
for (PrintSessionListener l : listeners) {
|
||||||
|
try { l.onPrintJobComplete(event); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void firePrinterStatusChanged(int code, String message) {
|
||||||
|
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_STATUS_CHANGED, null,
|
||||||
|
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), code, message, null);
|
||||||
|
for (PrintSessionListener l : listeners) {
|
||||||
|
try { l.onPrinterStatusChanged(event); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void firePrinterError(int errorCode, String errorMessage) {
|
||||||
|
PrintSessionEvent event = new PrintSessionEvent(this, PrintSessionEvent.EVENT_ERROR, null,
|
||||||
|
pd.getPageCount(), pd.getByteCount(), pd.getPageCount(), errorCode, errorMessage, null);
|
||||||
|
for (PrintSessionListener l : listeners) {
|
||||||
|
try { l.onPrinterError(event); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatus(int newCode, String msg) {
|
||||||
|
this.statusCode = newCode;
|
||||||
|
firePrinterStatusChanged(newCode, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Accessors ==========
|
||||||
|
|
||||||
|
public boolean isConnected() { return connected; }
|
||||||
|
public int getStatusCode() { return statusCode; }
|
||||||
|
public short getActiveLuType() { return activeLuType; }
|
||||||
|
public String getAssignedLuName() { return assignedLuName; }
|
||||||
|
public String getNegotiatedDeviceType() { return negotiatedDeviceType; }
|
||||||
|
|
||||||
|
public PrinterConfig getConfig() { return config; }
|
||||||
|
public PD3270 getPD() { return pd; }
|
||||||
|
public EbcdicTranslator getTranslator() { return translator; }
|
||||||
|
public PrintSCS3270 getSCS() { return scs; }
|
||||||
|
public PrintPS3270 getPrintPS() { return printPs; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level API Client for managing 3270/3287 Printer Sessions.
|
||||||
|
* Coordinates Telnet3270EP protocol engine, SCS/LU3 interpreters, and PD3270 print spoolers.
|
||||||
|
*/
|
||||||
|
public class Telnet3270EPClient {
|
||||||
|
|
||||||
|
private final PrinterConfig config;
|
||||||
|
private final Telnet3270EP protocolEngine;
|
||||||
|
|
||||||
|
public Telnet3270EPClient(PrinterConfig config) {
|
||||||
|
this.config = config != null ? config : new PrinterConfig();
|
||||||
|
this.protocolEngine = new Telnet3270EP(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Telnet3270EPClient(String host, int port, String printerLuName) {
|
||||||
|
this(new PrinterConfig(host, port, printerLuName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to printer host asynchronously.
|
||||||
|
*/
|
||||||
|
public boolean connect() {
|
||||||
|
return protocolEngine.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from printer host.
|
||||||
|
*/
|
||||||
|
public void disconnect() {
|
||||||
|
protocolEngine.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return protocolEngine.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStatusCode() {
|
||||||
|
return protocolEngine.getStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPrintListener(PrintSessionListener listener) {
|
||||||
|
protocolEngine.addPrintListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePrintListener(PrintSessionListener listener) {
|
||||||
|
protocolEngine.removePrintListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrinterConfig getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Telnet3270EP getProtocolEngine() {
|
||||||
|
return protocolEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PD3270 getPD() {
|
||||||
|
return protocolEngine.getPD();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrintSCS3270 getSCS() {
|
||||||
|
return protocolEngine.getSCS();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrintPS3270 getPrintPS() {
|
||||||
|
return protocolEngine.getPrintPS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print raw data stream directly through active printer interpreter.
|
||||||
|
*/
|
||||||
|
public void printDirectBytes(byte[] data, int offset, int length) {
|
||||||
|
if (protocolEngine.getActiveLuType() == PrinterConstants.LU_TYPE_3_DS) {
|
||||||
|
protocolEngine.getPrintPS().process3270PrintDS(data, offset, length);
|
||||||
|
} else {
|
||||||
|
protocolEngine.getSCS().processHostData(data, offset, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print local file through active printer destination.
|
||||||
|
*/
|
||||||
|
public void printFile(File file) throws IOException {
|
||||||
|
if (file == null || !file.exists()) {
|
||||||
|
throw new IllegalArgumentException("File not found: " + file);
|
||||||
|
}
|
||||||
|
try (FileInputStream fis = new FileInputStream(file)) {
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
int r;
|
||||||
|
while ((r = fis.read(buf)) > 0) {
|
||||||
|
protocolEngine.getPD().writePrintBytes(buf, 0, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,12 @@ public final class DS3270Constants {
|
|||||||
public static final int CMD_EAU = 0x0f; // Erase All Unprotected
|
public static final int CMD_EAU = 0x0f; // Erase All Unprotected
|
||||||
public static final int CMD_WSF = 0x11; // Write Structured Field
|
public static final int CMD_WSF = 0x11; // Write Structured Field
|
||||||
|
|
||||||
|
// Command aliases
|
||||||
|
public static final int CMD_WRITE = CMD_W;
|
||||||
|
public static final int CMD_ERASE_WRITE = CMD_EW;
|
||||||
|
public static final int CMD_ERASE_WRITE_ALT = CMD_EWA;
|
||||||
|
public static final int CMD_ERASE_ALL_UNPROTECTED = CMD_EAU;
|
||||||
|
|
||||||
// SNA 3270 Commands
|
// SNA 3270 Commands
|
||||||
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
|
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
|
||||||
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
|
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ public class TlsTrustManager implements X509TrustManager {
|
|||||||
|
|
||||||
public TlsTrustManager(ConnectionConfig config) {
|
public TlsTrustManager(ConnectionConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
initDefaultTrustManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TlsTrustManager(boolean tlsVerifyCert, TlsCertificateVerifier verifier) {
|
||||||
|
ConnectionConfig cfg = new ConnectionConfig();
|
||||||
|
cfg.setTlsVerifyCert(tlsVerifyCert);
|
||||||
|
cfg.setCertificateVerifier(verifier);
|
||||||
|
this.config = cfg;
|
||||||
|
initDefaultTrustManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initDefaultTrustManager() {
|
||||||
try {
|
try {
|
||||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
tmf.init((KeyStore) null);
|
tmf.init((KeyStore) null);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class PD3270Test {
|
||||||
|
|
||||||
|
private PD3270 pd;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
pd = new PD3270();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMemoryCaptureAndPageCount() {
|
||||||
|
assertTrue(pd.openPrinter(null));
|
||||||
|
assertTrue(pd.isOpen());
|
||||||
|
|
||||||
|
pd.writePrintLine("Line 1");
|
||||||
|
pd.writePrintLine("Line 2");
|
||||||
|
pd.formFeed();
|
||||||
|
|
||||||
|
assertEquals(1, pd.getPageCount());
|
||||||
|
assertTrue(pd.getByteCount() > 0);
|
||||||
|
|
||||||
|
String text = pd.getCapturedText();
|
||||||
|
assertTrue(text.contains("Line 1"));
|
||||||
|
assertTrue(text.contains("Line 2"));
|
||||||
|
assertTrue(text.contains("\f"));
|
||||||
|
|
||||||
|
pd.closePrinter();
|
||||||
|
assertFalse(pd.isOpen());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFileOutput() throws IOException {
|
||||||
|
File tempFile = File.createTempFile("printer_test_", ".txt");
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
|
||||||
|
PrinterConfig config = new PrinterConfig();
|
||||||
|
config.setDestinationType(PrinterConfig.DestinationType.FILE);
|
||||||
|
config.setDestinationTarget(tempFile.getAbsolutePath());
|
||||||
|
|
||||||
|
PD3270 filePd = new PD3270(config);
|
||||||
|
assertTrue(filePd.openPrinter(tempFile.getAbsolutePath()));
|
||||||
|
|
||||||
|
filePd.writePrintLine("Test Output Line");
|
||||||
|
filePd.closePrinter();
|
||||||
|
|
||||||
|
String fileContent = new String(Files.readAllBytes(tempFile.toPath()));
|
||||||
|
assertTrue(fileContent.contains("Test Output Line"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class PrintPS3270Test {
|
||||||
|
|
||||||
|
private PrinterConfig config;
|
||||||
|
private PD3270 pd;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private PrintPS3270 printPs;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
config = new PrinterConfig("localhost", 23);
|
||||||
|
pd = new PD3270(config);
|
||||||
|
translator = new EbcdicTranslator("037");
|
||||||
|
printPs = new PrintPS3270(config, pd, translator);
|
||||||
|
pd.openPrinter(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEraseWriteAndStartPrint() {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(DS3270Constants.CMD_ERASE_WRITE);
|
||||||
|
out.write(PrinterConstants.WCC_START_PRINT_BIT); // WCC with Start Print bit
|
||||||
|
|
||||||
|
// SBA 0, 0
|
||||||
|
out.write(PrinterConstants.ORDER_SBA);
|
||||||
|
out.write(0x40); out.write(0x40); // 0, 0 address
|
||||||
|
|
||||||
|
// Write EBCDIC "REPORT"
|
||||||
|
byte[] rep = translator.stringToEbcdic("REPORT");
|
||||||
|
out.write(rep, 0, rep.length);
|
||||||
|
|
||||||
|
byte[] payload = out.toByteArray();
|
||||||
|
printPs.process3270PrintDS(payload, 0, payload.length);
|
||||||
|
|
||||||
|
String captured = pd.getCapturedText();
|
||||||
|
assertTrue(captured.contains("REPORT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRepeatToAddressOrder() {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(DS3270Constants.CMD_WRITE);
|
||||||
|
out.write(0x00); // WCC
|
||||||
|
|
||||||
|
out.write(PrinterConstants.ORDER_SBA);
|
||||||
|
out.write(0x40); out.write(0x40); // 0
|
||||||
|
|
||||||
|
// Repeat '*' to address 10
|
||||||
|
out.write(PrinterConstants.ORDER_RA);
|
||||||
|
out.write(0x40); out.write(0x4A); // Address 10
|
||||||
|
out.write(translator.unicodeToEbcdic('*'));
|
||||||
|
|
||||||
|
byte[] payload = out.toByteArray();
|
||||||
|
printPs.process3270PrintDS(payload, 0, payload.length);
|
||||||
|
printPs.flushPrintBuffer();
|
||||||
|
|
||||||
|
String captured = pd.getCapturedText();
|
||||||
|
assertTrue(captured.contains("**********"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFieldAttributesAndColorCalculations() {
|
||||||
|
assertEquals(1, printPs.calculateColor(0xF1)); // Blue
|
||||||
|
assertEquals(2, printPs.calculateColor(0xF2)); // Red
|
||||||
|
assertEquals(4, printPs.calculateColor(0xF4)); // Green
|
||||||
|
assertEquals(0, printPs.calculateColor(0x00)); // Neutral
|
||||||
|
|
||||||
|
assertEquals(1, printPs.calculateHighlight(PrinterConstants.SEAC_BLINK));
|
||||||
|
assertEquals(2, printPs.calculateHighlight(PrinterConstants.SEAC_REVERSE));
|
||||||
|
assertEquals(4, printPs.calculateHighlight(PrinterConstants.SEAC_UNDERLINE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class PrintSCS3270Test {
|
||||||
|
|
||||||
|
private PrinterConfig config;
|
||||||
|
private PD3270 pd;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private PrintSCS3270 scs;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
config = new PrinterConfig("localhost", 23);
|
||||||
|
pd = new PD3270(config);
|
||||||
|
translator = new EbcdicTranslator("037");
|
||||||
|
scs = new PrintSCS3270(config, pd, translator);
|
||||||
|
pd.openPrinter(null); // In-memory capture
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasicPrintAndNewLines() {
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
// EBCDIC "HELLO" + NL + "WORLD" + FF
|
||||||
|
byte[] helloEbc = translator.stringToEbcdic("HELLO");
|
||||||
|
byte[] worldEbc = translator.stringToEbcdic("WORLD");
|
||||||
|
|
||||||
|
stream.write(helloEbc, 0, helloEbc.length);
|
||||||
|
stream.write(PrinterConstants.SCS_NL);
|
||||||
|
stream.write(worldEbc, 0, worldEbc.length);
|
||||||
|
stream.write(PrinterConstants.SCS_FF);
|
||||||
|
|
||||||
|
byte[] payload = stream.toByteArray();
|
||||||
|
scs.processHostData(payload, 0, payload.length);
|
||||||
|
|
||||||
|
String text = pd.getCapturedText();
|
||||||
|
assertTrue(text.contains("HELLO"));
|
||||||
|
assertTrue(text.contains("WORLD"));
|
||||||
|
assertEquals(1, pd.getPageCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetHorizontalFormatAndTabs() {
|
||||||
|
// SHF order: 0x2B 0xD1 <len> <mpp> <lm> <rm> <tab1> <tab2>
|
||||||
|
byte[] shfOrder = new byte[]{
|
||||||
|
(byte) PrinterConstants.SCS_PREFIX_2B,
|
||||||
|
(byte) PrinterConstants.SCS_SHF,
|
||||||
|
0x08, // Length
|
||||||
|
(byte) 132, // MPP = 132
|
||||||
|
0x05, // Left margin = 5
|
||||||
|
(byte) 120, // Right margin = 120
|
||||||
|
0x10, // Tab 1 = 16
|
||||||
|
0x20 // Tab 2 = 32
|
||||||
|
};
|
||||||
|
|
||||||
|
scs.processHostData(shfOrder, 0, shfOrder.length);
|
||||||
|
assertEquals(132, scs.getLineLength());
|
||||||
|
assertEquals(5, scs.getLeftMargin());
|
||||||
|
assertEquals(120, scs.getRightMargin());
|
||||||
|
|
||||||
|
int nextTab = scs.calculateHorizontalTab(10);
|
||||||
|
assertEquals(16, nextTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetVerticalFormatAndTabs() {
|
||||||
|
// SVF order: 0x2B 0xD2 <len> <mpl> <tm> <bm> <tab1> <tab2>
|
||||||
|
byte[] svfOrder = new byte[]{
|
||||||
|
(byte) PrinterConstants.SCS_PREFIX_2B,
|
||||||
|
(byte) PrinterConstants.SCS_SVF,
|
||||||
|
0x08, // Length
|
||||||
|
(byte) 88, // MPL = 88
|
||||||
|
0x03, // Top margin = 3
|
||||||
|
(byte) 80, // Bottom margin = 80
|
||||||
|
0x0A, // Tab 1 = 10
|
||||||
|
0x14 // Tab 2 = 20
|
||||||
|
};
|
||||||
|
|
||||||
|
scs.processHostData(svfOrder, 0, svfOrder.length);
|
||||||
|
assertEquals(88, scs.getPageLength());
|
||||||
|
assertEquals(3, scs.getTopMargin());
|
||||||
|
assertEquals(80, scs.getBottomMargin());
|
||||||
|
|
||||||
|
int nextVTab = scs.calculateVerticalTab(5);
|
||||||
|
assertEquals(10, nextVTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoubleWidthAndHighlighting() {
|
||||||
|
scs.startDoubleWidthCharacters();
|
||||||
|
assertTrue(scs.isDoubleWidth());
|
||||||
|
|
||||||
|
scs.setEnhancedHighlight(PrinterConstants.SEAC_UNDERLINE);
|
||||||
|
assertEquals(PrinterConstants.SEAC_UNDERLINE, scs.getActiveHighlight());
|
||||||
|
|
||||||
|
scs.endDoubleWidthCharacters();
|
||||||
|
assertFalse(scs.isDoubleWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPPAandPPVPositioning() {
|
||||||
|
// PPA absolute to column 25: 0x2B 0xC6 0x02 0x01 0x19
|
||||||
|
byte[] ppa = new byte[]{
|
||||||
|
(byte) PrinterConstants.SCS_PREFIX_2B,
|
||||||
|
(byte) PrinterConstants.SCS_PPA,
|
||||||
|
0x02,
|
||||||
|
PrinterConstants.POS_ABSOLUTE,
|
||||||
|
0x19 // col 25
|
||||||
|
};
|
||||||
|
scs.processHostData(ppa, 0, ppa.length);
|
||||||
|
assertEquals(25, scs.getCurrentColumn());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransparentStream() {
|
||||||
|
// TRS: 0x35 <len> <raw data>
|
||||||
|
byte[] trs = new byte[]{
|
||||||
|
PrinterConstants.SCS_TRS,
|
||||||
|
0x04,
|
||||||
|
'T', 'E', 'S', 'T'
|
||||||
|
};
|
||||||
|
scs.processHostData(trs, 0, trs.length);
|
||||||
|
assertTrue(pd.getByteCount() >= 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class PrinterPhase6Test {
|
||||||
|
|
||||||
|
private PrinterConfig printerConfig;
|
||||||
|
private Telnet3270EP printer;
|
||||||
|
private PD3270 pd;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
printerConfig = new PrinterConfig("localhost", 23, "PRT_LU1");
|
||||||
|
printerConfig.setAssociatedDisplayLuName("DSP_LU1");
|
||||||
|
pd = new PD3270(printerConfig);
|
||||||
|
translator = new EbcdicTranslator("037");
|
||||||
|
printer = new Telnet3270EP(printerConfig, pd, translator);
|
||||||
|
pd.openPrinter(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConnectionConfigToPrinterConfigMapping() {
|
||||||
|
ConnectionConfig ccfg = new ConnectionConfig("10.0.0.1", 992, true);
|
||||||
|
ccfg.setLuName("DSP01");
|
||||||
|
ccfg.setAssociatedPrinterLu("PRT01");
|
||||||
|
ccfg.setCodePage("1047");
|
||||||
|
|
||||||
|
PrinterConfig pcfg = ccfg.toPrinterConfig();
|
||||||
|
assertEquals("10.0.0.1", pcfg.getHost());
|
||||||
|
assertEquals(992, pcfg.getPort());
|
||||||
|
assertTrue(pcfg.isUseTls());
|
||||||
|
assertEquals("PRT01", pcfg.getPrinterLuName());
|
||||||
|
assertEquals("DSP01", pcfg.getAssociatedDisplayLuName());
|
||||||
|
assertEquals("1047", pcfg.getCodePage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDataStreamProcessorEmbeddedScsRouting() {
|
||||||
|
ScreenBuffer screen = new ScreenBuffer(haus.nightmare.lib3270j.TerminalModel.IBM_3279_4, translator);
|
||||||
|
DataStreamProcessor dsp = new DataStreamProcessor(screen, translator);
|
||||||
|
PrintSCS3270 scs = new PrintSCS3270(printerConfig, pd, translator);
|
||||||
|
|
||||||
|
dsp.setEmbeddedScsProcessor(scs);
|
||||||
|
assertSame(scs, dsp.getEmbeddedScsProcessor());
|
||||||
|
|
||||||
|
byte[] helloEbc = translator.stringToEbcdic("EMBEDDED SCS");
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
stream.write(helloEbc, 0, helloEbc.length);
|
||||||
|
stream.write(PrinterConstants.SCS_NL);
|
||||||
|
|
||||||
|
byte[] payload = stream.toByteArray();
|
||||||
|
dsp.processSCSData(payload, 0, payload.length);
|
||||||
|
|
||||||
|
String text = pd.getCapturedText();
|
||||||
|
assertTrue(text.contains("EMBEDDED SCS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQueryReplyBuilderAuxDevice() {
|
||||||
|
ScreenBuffer screen = new ScreenBuffer(haus.nightmare.lib3270j.TerminalModel.IBM_3279_4, translator);
|
||||||
|
haus.nightmare.lib3270j.datastream.QueryReplyBuilder qrb = new haus.nightmare.lib3270j.datastream.QueryReplyBuilder(screen);
|
||||||
|
|
||||||
|
byte[] auxDev = qrb.buildAuxDevice();
|
||||||
|
assertNotNull(auxDev);
|
||||||
|
assertTrue(auxDev.length >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTelnet3270EPClientFacade() {
|
||||||
|
Telnet3270EPClient client = new Telnet3270EPClient(printerConfig);
|
||||||
|
assertNotNull(client.getPD());
|
||||||
|
assertNotNull(client.getSCS());
|
||||||
|
assertNotNull(client.getPrintPS());
|
||||||
|
assertNotNull(client.getConfig());
|
||||||
|
|
||||||
|
// Test direct print through client
|
||||||
|
byte[] testText = translator.stringToEbcdic("CLIENT PRINT");
|
||||||
|
client.printDirectBytes(testText, 0, testText.length);
|
||||||
|
client.getSCS().flushLineBuffer();
|
||||||
|
|
||||||
|
String captured = client.getPD().getCapturedText();
|
||||||
|
assertTrue(captured.contains("CLIENT PRINT"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package haus.nightmare.lib3270j.printer;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class Telnet3270EPTest {
|
||||||
|
|
||||||
|
private PrinterConfig config;
|
||||||
|
private PD3270 pd;
|
||||||
|
private Telnet3270EP printer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
config = new PrinterConfig("localhost", 23, "PRT01");
|
||||||
|
config.setAssociatedDisplayLuName("DSP01");
|
||||||
|
pd = new PD3270(config);
|
||||||
|
printer = new Telnet3270EP(config, pd, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrinterConfigAndDefaults() {
|
||||||
|
assertNotNull(config);
|
||||||
|
assertEquals("PRT01", config.getPrinterLuName());
|
||||||
|
assertEquals("DSP01", config.getAssociatedDisplayLuName());
|
||||||
|
assertEquals(PrinterConstants.DEV_IBM_3287_1, config.getDeviceType());
|
||||||
|
assertEquals(80, config.getMpp());
|
||||||
|
assertEquals(66, config.getMpl());
|
||||||
|
assertTrue(config.isFormFeedAtEoj());
|
||||||
|
assertTrue(config.isAutoFlushOnEoj());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBindProcessingLU1andLU3() {
|
||||||
|
// Test LU-1 SCS bind
|
||||||
|
printer.process_bind(PrinterConstants.LU_TYPE_1_SCS);
|
||||||
|
assertEquals(PrinterConstants.LU_TYPE_1_SCS, printer.getActiveLuType());
|
||||||
|
assertEquals(PrinterConstants.STATUS_PRINTER_READY, printer.getStatusCode());
|
||||||
|
|
||||||
|
// Test LU-3 3270 DS bind
|
||||||
|
printer.process_bind(PrinterConstants.LU_TYPE_3_DS);
|
||||||
|
assertEquals(PrinterConstants.LU_TYPE_3_DS, printer.getActiveLuType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBindImagePayloadParsing() {
|
||||||
|
byte[] bindImage = new byte[30];
|
||||||
|
bindImage[14] = 0x01; // LU1 profile
|
||||||
|
printer.processBindImage(bindImage, 0, bindImage.length);
|
||||||
|
assertEquals(PrinterConstants.LU_TYPE_1_SCS, printer.getActiveLuType());
|
||||||
|
|
||||||
|
bindImage[14] = 0x03; // LU3 profile
|
||||||
|
printer.processBindImage(bindImage, 0, bindImage.length);
|
||||||
|
assertEquals(PrinterConstants.LU_TYPE_3_DS, printer.getActiveLuType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEOJAndListeners() {
|
||||||
|
List<PrintSessionEvent> events = new ArrayList<>();
|
||||||
|
printer.addPrintListener(new PrintSessionListener() {
|
||||||
|
@Override public void onPrintJobStarted(PrintSessionEvent event) { events.add(event); }
|
||||||
|
@Override public void onPrintJobData(PrintSessionEvent event) { events.add(event); }
|
||||||
|
@Override public void onPrintJobPageComplete(PrintSessionEvent event) { events.add(event); }
|
||||||
|
@Override public void onPrintJobComplete(PrintSessionEvent event) { events.add(event); }
|
||||||
|
@Override public void onPrinterStatusChanged(PrintSessionEvent event) { events.add(event); }
|
||||||
|
@Override public void onPrinterError(PrintSessionEvent event) { events.add(event); }
|
||||||
|
});
|
||||||
|
|
||||||
|
printer.firePrintJobStarted("JOB001");
|
||||||
|
printer.sendEOJ(true);
|
||||||
|
|
||||||
|
assertEquals(PrinterConstants.STATUS_JOB_COMPLETE, printer.getStatusCode());
|
||||||
|
assertTrue(events.size() >= 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -62,13 +62,13 @@ fi
|
|||||||
# 4. If JUnit standalone runner is available, compile & run tests directly
|
# 4. If JUnit standalone runner is available, compile & run tests directly
|
||||||
if [ -f "$JUNIT_JAR" ] && [ -s "$JUNIT_JAR" ]; then
|
if [ -f "$JUNIT_JAR" ] && [ -s "$JUNIT_JAR" ]; then
|
||||||
echo "Compiling tests..."
|
echo "Compiling tests..."
|
||||||
find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt"
|
find "$SCRIPT_DIR/lib3270j/src/test/java" "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt"
|
||||||
"$JAVAC_BIN" -cp "$BUILD_DIR/lib3270j:$JUNIT_JAR" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
|
"$JAVAC_BIN" -cp "$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_JAR" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
|
||||||
rm -f "$BUILD_DIR/test_sources.txt"
|
rm -f "$BUILD_DIR/test_sources.txt"
|
||||||
|
|
||||||
echo "Executing JUnit tests..."
|
echo "Executing JUnit tests..."
|
||||||
"$JAVA_BIN" -jar "$JUNIT_JAR" \
|
"$JAVA_BIN" -Djava.awt.headless=true -jar "$JUNIT_JAR" \
|
||||||
--class-path "$BUILD_DIR/lib3270j:$TEST_BUILD_DIR" \
|
--class-path "$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$TEST_BUILD_DIR" \
|
||||||
--scan-class-path \
|
--scan-class-path \
|
||||||
--reports-dir="$REPORTS_DIR"
|
--reports-dir="$REPORTS_DIR"
|
||||||
echo "=== All Tests Passed Successfully ==="
|
echo "=== All Tests Passed Successfully ==="
|
||||||
|
|||||||
Reference in New Issue
Block a user