This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
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.ScreenUpdateListener;
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.j3270.ui.StatusBar;
|
||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.j3270.ui.*;
|
||||
import haus.nightmare.j3270.ft.FileTransfer;
|
||||
import haus.nightmare.j3270.ft.FileTransferDialog;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.event.*;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.*;
|
||||
|
||||
@@ -36,6 +39,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
private FileTransfer fileTransfer;
|
||||
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() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
@@ -52,7 +61,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
refreshTimer = new Timer(100, e -> {
|
||||
if (client != null) {
|
||||
statusBar.updateStatus();
|
||||
// Keep focus on terminal panel when window is active
|
||||
if (isActive() && !terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
@@ -66,12 +74,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
disconnect();
|
||||
if (refreshTimer != null)
|
||||
refreshTimer.stop();
|
||||
if (printerSessionDialog != null) {
|
||||
printerSessionDialog.dispose();
|
||||
}
|
||||
terminalPanel.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
// When window gains focus, push to terminal panel
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
@@ -79,11 +89,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
private void buildUI() {
|
||||
terminalPanel = new TerminalPanel();
|
||||
|
||||
// Status bar
|
||||
statusBar = new StatusBar();
|
||||
|
||||
// Layout
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().setBackground(new Color(10, 10, 10));
|
||||
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.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");
|
||||
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
||||
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
||||
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.addSeparator();
|
||||
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
||||
@@ -108,7 +121,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}));
|
||||
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");
|
||||
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> 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.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);
|
||||
|
||||
// Actions menu
|
||||
// 4. Actions menu
|
||||
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, () -> {
|
||||
if (client != null)
|
||||
client.sendClear();
|
||||
@@ -130,17 +214,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
client.reset();
|
||||
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.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
||||
terminalPanel.toggleLightPen();
|
||||
}, true));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
// Help menu
|
||||
// 5. Help menu
|
||||
JMenu helpMenu = createMenu("Help");
|
||||
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));
|
||||
menuBar.add(helpMenu);
|
||||
|
||||
@@ -169,6 +277,164 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
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() {
|
||||
if (client == null) {
|
||||
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.setMillisToPopup(0);
|
||||
|
||||
// Start a timer to check for manual cancellation
|
||||
Timer cancelCheckTimer = new Timer(500, e -> {
|
||||
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
@@ -208,7 +473,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
public void onBytesTransferred(long bytes) {
|
||||
if (monitor != null) {
|
||||
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();
|
||||
lastPort = config.getPort();
|
||||
|
||||
// Save for auto-connect
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
||||
|
||||
// Disconnect existing connection
|
||||
disconnect();
|
||||
|
||||
// Wire interactive certificate verifier if none configured
|
||||
if (config.getCertificateVerifier() == null) {
|
||||
config.setCertificateVerifier((chain, authType, 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]") : "";
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||
|
||||
// Resize window to match model
|
||||
terminalPanel.guardedPack();
|
||||
|
||||
new Thread(() -> {
|
||||
@@ -309,7 +569,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
}, "Connect-Thread").start();
|
||||
|
||||
// Focus the terminal panel
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
|
||||
@@ -332,8 +591,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
int current = terminalPanel.getFontSize();
|
||||
int newSize = Math.max(8, Math.min(72, current + delta));
|
||||
terminalPanel.setFontSize(newSize);
|
||||
// Pack after font change — setFontSize sets the resize guard
|
||||
// to prevent the componentResized from re-triggering autoFitFont
|
||||
terminalPanel.guardedPack();
|
||||
}
|
||||
|
||||
@@ -346,7 +603,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
statusBar.updateStatus();
|
||||
terminalPanel.repaint();
|
||||
|
||||
// Auto-resize and focus on first full session
|
||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||
terminalPanel.guardedPack();
|
||||
terminalPanel.requestFocusInWindow();
|
||||
@@ -367,6 +623,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
if (deviceName != null) {
|
||||
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
||||
}
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -377,10 +634,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
if (screenUpdatePending.compareAndSet(false, true)) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
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) {
|
||||
fileTransfer.onScreenUpdated();
|
||||
}
|
||||
@@ -402,6 +655,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
terminalPanel.guardedPack();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -423,11 +677,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
"Insert — Toggle insert mode\n" +
|
||||
"Escape — Reset\n" +
|
||||
"PageUp/Down — PF7/PF8\n" +
|
||||
"Alt+C — Clear\n" +
|
||||
"Alt+1/2/3 — PA1/PA2/PA3\n" +
|
||||
"Cmd+D — Disconnect\n" +
|
||||
"Cmd+Q — Quit\n" +
|
||||
"Cmd+=/-/0 — Font size +/-/reset";
|
||||
"Alt+C / Ctrl+K — Clear\n" +
|
||||
"Alt+E — Erase Input\n" +
|
||||
"Alt+A — Attention\n" +
|
||||
"Alt+S — System Request\n" +
|
||||
"Alt+Q — Cursor Select\n" +
|
||||
"Alt+L — Toggle Light Pen\n" +
|
||||
"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);
|
||||
area.setEditable(false);
|
||||
@@ -435,18 +696,56 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
area.setBackground(new Color(30, 30, 30));
|
||||
area.setForeground(new Color(200, 200, 200));
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"j3270 — Java TN3270 Terminal Emulator\n\n" +
|
||||
"A Java reimplementation of x3270.\n" +
|
||||
"lib3270j v0.1.0\n\n" +
|
||||
"Supports: TN3270, TN3270E (RFC 2355)\n" +
|
||||
"Models: IBM 3278/3279 Models 2-5\n" +
|
||||
"Colors, Extended Attributes, Query Replies",
|
||||
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" +
|
||||
"lib3270j v0.2.0\n\n" +
|
||||
"Features:\n" +
|
||||
"• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" +
|
||||
"• 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);
|
||||
}
|
||||
|
||||
@@ -457,7 +756,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
boolean cliTls = false;
|
||||
boolean cliNoVerifyCert = false;
|
||||
Boolean cliTn3270e = null;
|
||||
haus.nightmare.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
||||
GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
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])) {
|
||||
cliTn3270e = true;
|
||||
} 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("-")) {
|
||||
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
|
||||
cliGraphicsMode = GraphicsMode.fromString(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) {
|
||||
configFile = args[++i];
|
||||
} 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;
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
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());
|
||||
}
|
||||
|
||||
// Load INI config file if specified
|
||||
if (configFile != null) {
|
||||
try {
|
||||
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
||||
@@ -535,43 +832,38 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
}
|
||||
|
||||
// Dark look and feel
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception e) {
|
||||
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.awt.application.name", "j3270");
|
||||
|
||||
final boolean finalTls = cliTls;
|
||||
final boolean finalNoVerify = cliNoVerifyCert;
|
||||
final Boolean finalTn3270e = cliTn3270e;
|
||||
final haus.nightmare.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
J3270App app = new J3270App();
|
||||
app.setVisible(true);
|
||||
|
||||
// If host:port given on command line, connect directly
|
||||
if (!remainingArgs.isEmpty()) {
|
||||
String hostArg = remainingArgs.get(0);
|
||||
int port = finalTls ? 992 : 23;
|
||||
if (remainingArgs.size() >= 2) {
|
||||
try {
|
||||
port = Integer.parseInt(remainingArgs.get(1));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
if (remainingArgs.size() >= 3) {
|
||||
try {
|
||||
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
||||
model = TerminalModel.forModel(modelNum, true);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
||||
if (finalTls) {
|
||||
|
||||
@@ -53,6 +53,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new file transfer with the given configuration.
|
||||
* @return null if started successfully, error message otherwise.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
@@ -18,6 +20,8 @@ public class FileTransferDialog extends JDialog {
|
||||
private JTextField localFileField;
|
||||
private JButton browseLocalButton;
|
||||
private JTextField hostFileField;
|
||||
private JButton browseHostButton;
|
||||
private JComboBox<Integer> mtuCombo;
|
||||
|
||||
// Options
|
||||
private JRadioButton asciiRadio;
|
||||
@@ -40,7 +44,7 @@ public class FileTransferDialog extends JDialog {
|
||||
private JButton cancelButton;
|
||||
|
||||
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.coordinator = coordinator;
|
||||
|
||||
@@ -76,7 +80,6 @@ public class FileTransferDialog extends JDialog {
|
||||
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
|
||||
dirPanel.add(sendRadio);
|
||||
|
||||
// Disable append/overwrite when sending
|
||||
sendRadio.addActionListener(e -> updateOptionStates());
|
||||
receiveRadio.addActionListener(e -> updateOptionStates());
|
||||
|
||||
@@ -94,7 +97,13 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
// Host File
|
||||
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
|
||||
asciiRadio = new JRadioButton("ASCII");
|
||||
@@ -114,6 +123,11 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
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)
|
||||
crCheck = new JCheckBox("Add/Remove CR");
|
||||
crCheck.setSelected(true);
|
||||
@@ -198,10 +212,7 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
setContentPane(mainPanel);
|
||||
|
||||
// Theme styling for components
|
||||
applyTheme(mainPanel);
|
||||
|
||||
// Initialize state
|
||||
updateOptionStates();
|
||||
}
|
||||
|
||||
@@ -236,14 +247,29 @@ public class FileTransferDialog extends JDialog {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
|
||||
|
||||
// Auto-fill host file if empty
|
||||
if (hostFileField.getText().trim().isEmpty()) {
|
||||
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() {
|
||||
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
|
||||
@@ -262,6 +288,11 @@ public class FileTransferDialog extends JDialog {
|
||||
config.setAppend(appendCheck.isSelected());
|
||||
config.setOverwrite(overwriteCheck.isSelected());
|
||||
|
||||
Integer mtu = (Integer) mtuCombo.getSelectedItem();
|
||||
if (mtu != null) {
|
||||
config.setDftBufferSize(mtu);
|
||||
}
|
||||
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setBlksize(blksizeField.getText().trim());
|
||||
@@ -272,7 +303,6 @@ public class FileTransferDialog extends JDialog {
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||
} else {
|
||||
// Success, close dialog (progress will be shown separately)
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ public class ConnectDialog extends JDialog {
|
||||
buttonPanel.add(connectBtn);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 8;
|
||||
gbc.gridy = 9;
|
||||
gbc.gridwidth = 2;
|
||||
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.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Status bar displaying connection state, cursor position, timing, and lock
|
||||
* status.
|
||||
* Status bar displaying connection state, cursor position, timing, and lock status.
|
||||
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
|
||||
*/
|
||||
public class StatusBar extends JPanel {
|
||||
|
||||
private final JLabel connectionStatus;
|
||||
private final JLabel tlsStatus;
|
||||
private final JLabel cursorPosition;
|
||||
private final JLabel luName;
|
||||
private final JLabel lockStatus;
|
||||
private final JLabel fieldTypeStatus;
|
||||
private final JLabel codePageInfo;
|
||||
private final JLabel modelInfo;
|
||||
private final JButton lpButton;
|
||||
private final JLabel cursorPosition;
|
||||
|
||||
private Telnet3270Client client;
|
||||
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_DIM = new Color(80, 80, 80);
|
||||
private static final Color OIA_ALERT = new Color(255, 80, 80);
|
||||
private static final Color OIA_WARN = new Color(255, 200, 80);
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
@@ -43,33 +45,24 @@ public class StatusBar extends JPanel {
|
||||
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
||||
luName = createLabel("", oiaFont, OIA_FG);
|
||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
|
||||
codePageInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
cursorPosition = createLabel("001/001", 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"));
|
||||
}
|
||||
});
|
||||
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG);
|
||||
|
||||
add(Box.createHorizontalStrut(6));
|
||||
add(connectionStatus);
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(tlsStatus);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(luName);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lockStatus);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lpButton);
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(fieldTypeStatus);
|
||||
add(Box.createHorizontalGlue());
|
||||
add(codePageInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(modelInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(cursorPosition);
|
||||
@@ -86,15 +79,22 @@ public class StatusBar extends JPanel {
|
||||
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.setOnLightPenToggle(this::updateStatus);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Connection state
|
||||
ConnectionState state = client.getConnectionState();
|
||||
@@ -128,7 +128,7 @@ public class StatusBar extends JPanel {
|
||||
break;
|
||||
case CONNECTED_UNBOUND:
|
||||
connectionStatus.setText("Unbound");
|
||||
connectionStatus.setForeground(new Color(255, 255, 80));
|
||||
connectionStatus.setForeground(OIA_WARN);
|
||||
break;
|
||||
default:
|
||||
connectionStatus.setText(state.name());
|
||||
@@ -156,16 +156,41 @@ public class StatusBar extends JPanel {
|
||||
tlsStatus.setToolTipText(null);
|
||||
}
|
||||
|
||||
// LU name
|
||||
// LU Name
|
||||
String lu = "";
|
||||
if (client.getConnectionState().isTn3270e()) {
|
||||
// lu would come from FSM
|
||||
if (client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null && !client.getTelnetFSM().getConnectedLu().isEmpty()) {
|
||||
lu = "LU:" + client.getTelnetFSM().getConnectedLu();
|
||||
} else if (client.getConfig().getLuName() != null && !client.getConfig().getLuName().isEmpty()) {
|
||||
lu = "LU:" + client.getConfig().getLuName();
|
||||
}
|
||||
luName.setText(lu);
|
||||
|
||||
// Lock status
|
||||
if (client.getInputProcessor().isKeyboardLocked()) {
|
||||
lockStatus.setText("X SYSTEM");
|
||||
// Lock / Inhibit status
|
||||
int inhibit = client.getOIA().getInputInhibited();
|
||||
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
|
||||
switch (inhibit) {
|
||||
case ECLConstants.INHIBIT_SYSTEM_LOCK:
|
||||
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);
|
||||
} else if (client.getInputProcessor().isInsertMode()) {
|
||||
lockStatus.setText("INSERT");
|
||||
@@ -174,19 +199,34 @@ public class StatusBar extends JPanel {
|
||||
lockStatus.setText("");
|
||||
}
|
||||
|
||||
// Model info
|
||||
modelInfo.setText(client.getConfig().getModel().getTerminalType());
|
||||
// Field status: Numeric vs Alphanumeric
|
||||
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();
|
||||
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 col = sb.getCursorCol() + 1;
|
||||
cursorPosition.setText(String.format("%03d/%03d", row, col));
|
||||
|
||||
// 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);
|
||||
}
|
||||
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||
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.
|
||||
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
|
||||
* attributes.
|
||||
* Supports colors, bold, underline, reverse, blink, GOCA vector graphics,
|
||||
* 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;
|
||||
|
||||
@@ -62,7 +63,12 @@ public class TerminalPanel extends JPanel {
|
||||
private boolean isDragging = false;
|
||||
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 long lastGraphicsUpdateCount = -1;
|
||||
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.
|
||||
* Any leftover pixels (from integer font sizing) are split evenly.
|
||||
*/
|
||||
private int getRenderOffsetX() {
|
||||
int termCols = 80;
|
||||
@@ -82,7 +87,6 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
/**
|
||||
* Compute the vertical render offset to center the grid within the panel.
|
||||
* Must use getDisplayRows() to match paintComponent's iteration.
|
||||
*/
|
||||
private int getRenderOffsetY() {
|
||||
int termRows = 24;
|
||||
@@ -94,17 +98,17 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
// Default Host color mapping
|
||||
public static final Color[] DEFAULT_HOST_COLORS = {
|
||||
new Color(0, 0, 0), // 0: Neutral Black
|
||||
new Color(80, 120, 255), // 1: Blue
|
||||
new Color(255, 50, 50), // 2: Red
|
||||
new Color(0, 0, 0), // 0: Neutral Black
|
||||
new Color(80, 120, 255), // 1: Blue
|
||||
new Color(255, 50, 50), // 2: Red
|
||||
new Color(255, 130, 180), // 3: Pink
|
||||
new Color(50, 205, 50), // 4: Green
|
||||
new Color(64, 224, 208), // 5: Turquoise
|
||||
new Color(255, 255, 80), // 6: Yellow
|
||||
new Color(50, 205, 50), // 4: Green
|
||||
new Color(64, 224, 208), // 5: Turquoise
|
||||
new Color(255, 255, 80), // 6: Yellow
|
||||
new Color(255, 255, 255), // 7: Neutral White
|
||||
new Color(0, 0, 0), // 8: Black
|
||||
new Color(30, 60, 180), // 9: Deep Blue
|
||||
new Color(255, 165, 0), // 10: Orange
|
||||
new Color(0, 0, 0), // 8: Black
|
||||
new Color(30, 60, 180), // 9: Deep Blue
|
||||
new Color(255, 165, 0), // 10: Orange
|
||||
new Color(180, 130, 255), // 11: Purple
|
||||
new Color(144, 238, 144), // 12: Pale Green
|
||||
new Color(175, 238, 238), // 13: Pale Turquoise
|
||||
@@ -128,8 +132,6 @@ public class TerminalPanel extends JPanel {
|
||||
setDoubleBuffered(true);
|
||||
setFocusTraversalKeysEnabled(false);
|
||||
|
||||
// Use key bindings instead of KeyListener for reliable key handling
|
||||
// This avoids focus/event issues with JScrollPane
|
||||
setOpaque(true);
|
||||
setupKeyBindings();
|
||||
setupFont();
|
||||
@@ -160,12 +162,6 @@ public class TerminalPanel extends JPanel {
|
||||
col = Math.max(0, Math.min(col, displayCols - 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 isSelectableField = false;
|
||||
if (sb.isFormatted()) {
|
||||
@@ -267,16 +263,7 @@ public class TerminalPanel extends JPanel {
|
||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||
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;
|
||||
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(
|
||||
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
||||
button,
|
||||
@@ -288,9 +275,6 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
int faPos = sb.findFieldAttribute(clickAddr);
|
||||
if (faPos >= 0) {
|
||||
@@ -344,23 +328,42 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
// ========== Selection / Copy-Paste ==========
|
||||
|
||||
private void clearSelection() {
|
||||
public void clearSelection() {
|
||||
selectionStartRow = -1;
|
||||
selectionStartCol = -1;
|
||||
selectionEndRow = -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 &&
|
||||
!(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() {
|
||||
if (!hasSelection() || client == null) return "";
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
@@ -371,7 +374,6 @@ public class TerminalPanel extends JPanel {
|
||||
int c1, c2;
|
||||
|
||||
if (blockSelectMode) {
|
||||
// Block mode: rectangular selection
|
||||
c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
|
||||
@@ -391,7 +393,6 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
return result.toString();
|
||||
} else {
|
||||
// Line/stream mode: flow from start to end
|
||||
int startAddr, endAddr;
|
||||
if (selectionStartRow < selectionEndRow ||
|
||||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||
@@ -422,7 +423,7 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private void copySelection() {
|
||||
public void copySelection() {
|
||||
String text = getSelectedText();
|
||||
if (!text.isEmpty()) {
|
||||
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;
|
||||
try {
|
||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
@@ -438,8 +439,6 @@ public class TerminalPanel extends JPanel {
|
||||
if (text != null) {
|
||||
for (char ch : text.toCharArray()) {
|
||||
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;
|
||||
}
|
||||
if (ch >= 0x20 && ch != 0x7F) {
|
||||
@@ -448,9 +447,35 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Clipboard not available or wrong type — silently ignore
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -466,6 +491,10 @@ public class TerminalPanel extends JPanel {
|
||||
pasteItem.addActionListener(ev -> pasteClipboard());
|
||||
popup.add(pasteItem);
|
||||
|
||||
JMenuItem selectAllItem = new JMenuItem("Select All");
|
||||
selectAllItem.addActionListener(ev -> selectAll());
|
||||
popup.add(selectAllItem);
|
||||
|
||||
popup.addSeparator();
|
||||
|
||||
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||
@@ -489,7 +518,6 @@ public class TerminalPanel extends JPanel {
|
||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
||||
} else {
|
||||
// Stream mode
|
||||
int cols = 80;
|
||||
if (client != null) cols = client.getScreenBuffer().getCols();
|
||||
int addr = row * cols + col;
|
||||
@@ -508,12 +536,6 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
// ========== 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() {
|
||||
int panelW = getWidth();
|
||||
int panelH = getHeight();
|
||||
@@ -527,12 +549,10 @@ public class TerminalPanel extends JPanel {
|
||||
termRows = sb.getDisplayRows();
|
||||
}
|
||||
|
||||
// Use minimal padding for the fit calculation
|
||||
int availW = panelW - 2 * padding;
|
||||
int availH = panelH - 2 * padding;
|
||||
if (availW <= 0 || availH <= 0) return;
|
||||
|
||||
// Find the largest font size where the grid fits
|
||||
int bestSize = 8;
|
||||
for (int testSize = 8; testSize <= 72; 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);
|
||||
updateCellSize();
|
||||
}
|
||||
// No window snap — any leftover pixels are centered via getRenderOffsetX/Y
|
||||
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() {
|
||||
resizeGuard = true;
|
||||
revalidate();
|
||||
@@ -572,16 +586,10 @@ public class TerminalPanel extends JPanel {
|
||||
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) {
|
||||
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Support multiple bindings separated by commas
|
||||
String[] bindings = bindingStr.split(",");
|
||||
for (String binding : bindings) {
|
||||
binding = binding.trim();
|
||||
@@ -601,7 +609,6 @@ public class TerminalPanel extends JPanel {
|
||||
im.clear();
|
||||
am.clear();
|
||||
|
||||
// Block the scroll pane from handling Tab, arrows, Page keys
|
||||
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
||||
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
||||
@@ -611,25 +618,16 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
String defaultBinding;
|
||||
if (i <= 12) {
|
||||
defaultBinding = "F" + i;
|
||||
} else {
|
||||
defaultBinding = "shift F" + (i - 12);
|
||||
}
|
||||
String defaultBinding = i <= 12 ? ("F" + i) : ("shift F" + (i - 12));
|
||||
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
||||
bindKeyToMap(im, "PF" + i, binding);
|
||||
}
|
||||
|
||||
// PA keys: Alt+1, Alt+2, Alt+3
|
||||
String pa1Def = "alt 1";
|
||||
String pa2Def = "alt 2";
|
||||
String pa3Def = "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));
|
||||
// PA keys
|
||||
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", "alt 1"));
|
||||
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", "alt 2"));
|
||||
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", "alt 3"));
|
||||
|
||||
// Clear
|
||||
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, "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();
|
||||
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_A, shortcutMask), "j3270-SELECTALL");
|
||||
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-ESCAPE", createAction(this::handleReset));
|
||||
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-CURSEL", createAction(this::handleCursorSelect));
|
||||
|
||||
// Copy/Paste actions
|
||||
am.put("j3270-COPY", createAction(this::copySelection));
|
||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||
am.put("j3270-SELECTALL", createAction(this::selectAll));
|
||||
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
||||
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
@@ -688,22 +687,29 @@ public class TerminalPanel extends JPanel {
|
||||
am.put("j3270-PA2", createAction(() -> handlePA(2)));
|
||||
am.put("j3270-PA3", createAction(() -> handlePA(3)));
|
||||
|
||||
// For printable character input, we override processKeyEvent
|
||||
enableEvents(AWTEvent.KEY_EVENT_MASK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processKeyEvent(KeyEvent e) {
|
||||
// Handle character typing via processKeyEvent to capture ALL typed chars
|
||||
if (e.getID() == KeyEvent.KEY_TYPED) {
|
||||
char ch = e.getKeyChar();
|
||||
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
||||
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.typeCharacter(ch);
|
||||
refreshScreen();
|
||||
e.consume();
|
||||
return;
|
||||
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);
|
||||
refreshScreen();
|
||||
e.consume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -722,14 +728,23 @@ public class TerminalPanel extends JPanel {
|
||||
// ========== Key action handlers ==========
|
||||
|
||||
private void handleEnter() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendEnter();
|
||||
refreshScreen();
|
||||
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();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReset() {
|
||||
if (client != null) {
|
||||
clearSearchHighlight();
|
||||
clearSelection();
|
||||
client.reset();
|
||||
refreshScreen();
|
||||
}
|
||||
@@ -737,10 +752,8 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
private void handleTab(boolean shift) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (shift)
|
||||
client.backTab();
|
||||
else
|
||||
client.tab();
|
||||
if (shift) client.backTab();
|
||||
else client.tab();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
@@ -748,21 +761,11 @@ public class TerminalPanel extends JPanel {
|
||||
private void handleCursor(String dir) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
switch (dir) {
|
||||
case "up":
|
||||
client.cursorUp();
|
||||
break;
|
||||
case "down":
|
||||
client.cursorDown();
|
||||
break;
|
||||
case "left":
|
||||
client.cursorLeft();
|
||||
break;
|
||||
case "right":
|
||||
client.cursorRight();
|
||||
break;
|
||||
case "home":
|
||||
client.cursorHome();
|
||||
break;
|
||||
case "up": client.cursorUp(); break;
|
||||
case "down": client.cursorDown(); break;
|
||||
case "left": client.cursorLeft(); break;
|
||||
case "right": client.cursorRight(); break;
|
||||
case "home": client.cursorHome(); break;
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
@@ -797,9 +800,16 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void handleBackspace() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.getInputProcessor().backspace();
|
||||
refreshScreen();
|
||||
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();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,12 +880,11 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshScreen() {
|
||||
public void refreshScreen() {
|
||||
if (client != null) {
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
}
|
||||
repaint();
|
||||
// Notify parent to update status bar too
|
||||
Container parent = getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof JFrame) {
|
||||
@@ -892,7 +901,6 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
||||
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
||||
// Fallback
|
||||
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
|
||||
}
|
||||
updateCellSize();
|
||||
@@ -1015,9 +1023,6 @@ public class TerminalPanel extends JPanel {
|
||||
public Dimension getPreferredSize() {
|
||||
if (client != null) {
|
||||
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 displayRows = sb.getDisplayRows();
|
||||
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_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
|
||||
// Clear entire panel with background color
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||
|
||||
// Compute centered offsets for the grid
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
|
||||
@@ -1077,7 +1080,6 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Track current field attribute for monochrome color decisions
|
||||
byte currentFA = 0;
|
||||
ExtendedAttribute currentFieldEa = null;
|
||||
|
||||
@@ -1089,7 +1091,6 @@ public class TerminalPanel extends JPanel {
|
||||
int x = ox + col * cellWidth;
|
||||
int y = oy + row * cellHeight;
|
||||
|
||||
// Determine colors and attributes
|
||||
Color fgColor;
|
||||
Color bgColor;
|
||||
boolean bold = false;
|
||||
@@ -1099,7 +1100,6 @@ public class TerminalPanel extends JPanel {
|
||||
if (ea.isFieldAttribute()) {
|
||||
currentFA = ea.fa;
|
||||
currentFieldEa = ea;
|
||||
// Selection highlight on field attribute cells
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
@@ -1119,20 +1119,14 @@ public class TerminalPanel extends JPanel {
|
||||
// Graphics rendition
|
||||
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
|
||||
if (gr != 0) {
|
||||
if ((gr & GR_INTENSIFY) != 0)
|
||||
bold = true;
|
||||
if ((gr & GR_UNDERLINE) != 0)
|
||||
underline = true;
|
||||
if ((gr & GR_REVERSE) != 0)
|
||||
reverse = true;
|
||||
if ((gr & GR_INTENSIFY) != 0) bold = true;
|
||||
if ((gr & GR_UNDERLINE) != 0) 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)
|
||||
// Modern UX: render '*' for typed characters so user sees length/digit count
|
||||
// Password fields
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
@@ -1149,14 +1143,12 @@ public class TerminalPanel extends JPanel {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply reverse video
|
||||
if (reverse) {
|
||||
Color tmp = fgColor;
|
||||
fgColor = bgColor;
|
||||
bgColor = tmp;
|
||||
}
|
||||
|
||||
// Draw background only if different from default panel bgColor or if inverted
|
||||
if (!bgColor.equals(this.bgColor) || reverse) {
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
@@ -1192,13 +1184,18 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw underline
|
||||
if (underline) {
|
||||
g2.setColor(fgColor);
|
||||
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);
|
||||
}
|
||||
|
||||
// Draw search highlight
|
||||
if (isCellSearchHighlighted(row, col)) {
|
||||
g2.setColor(SEARCH_HIGHLIGHT_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
|
||||
// Draw selection highlight
|
||||
if (isCellSelected(row, col)) {
|
||||
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) {
|
||||
int gocaX = client.getGocaDecoder().getGraphicCursorX();
|
||||
int gocaY = client.getGocaDecoder().getGraphicCursorY();
|
||||
@@ -1222,7 +1219,6 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
g2.setColor(Color.WHITE);
|
||||
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, py - 6, px, py + 6);
|
||||
g2.setPaintMode();
|
||||
@@ -1241,13 +1237,6 @@ public class TerminalPanel extends JPanel {
|
||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||
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) {
|
||||
@@ -1267,8 +1256,6 @@ public class TerminalPanel extends JPanel {
|
||||
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
|
||||
if (bg >= 0xf0 && bg <= 0xff) {
|
||||
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) {
|
||||
return bgColor;
|
||||
}
|
||||
@@ -1288,22 +1275,26 @@ public class TerminalPanel extends JPanel {
|
||||
if (blinkTimer != null)
|
||||
blinkTimer.stop();
|
||||
}
|
||||
private Runnable onLightPenToggle;
|
||||
|
||||
public void setOnLightPenToggle(Runnable callback) {
|
||||
this.onLightPenToggle = callback;
|
||||
}
|
||||
|
||||
public void toggleLightPen() {
|
||||
this.lightPenMode = !this.lightPenMode;
|
||||
System.out.println("Light Pen mode: " + (this.lightPenMode ? "ON" : "OFF"));
|
||||
if (onLightPenToggle != null) {
|
||||
onLightPenToggle.run();
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
|
||||
public boolean isLightPenMode() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user