Add bugfix for ATTN
This commit is contained in:
@@ -42,6 +42,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
// Dialog instances
|
||||
private FindDialog findDialog;
|
||||
private ScriptDialog scriptDialog;
|
||||
private MacroDialog macroDialog;
|
||||
private FieldInspectorDialog fieldInspectorDialog;
|
||||
private PrinterSessionDialog printerSessionDialog;
|
||||
|
||||
@@ -136,6 +137,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
if (findDialog != null && findDialog.isVisible()) return true;
|
||||
if (scriptDialog != null && scriptDialog.isVisible()) return true;
|
||||
if (macroDialog != null && macroDialog.isVisible()) return true;
|
||||
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
|
||||
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
|
||||
for (Window owned : getOwnedWindows()) {
|
||||
@@ -453,6 +455,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}, true));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
||||
actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true));
|
||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
@@ -559,6 +562,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
scriptDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showMacroDialog() {
|
||||
if (macroDialog == null) {
|
||||
macroDialog = new MacroDialog(this, client, terminalPanel);
|
||||
}
|
||||
macroDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showFieldInspectorDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
@@ -800,6 +810,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
if (macroDialog != null) {
|
||||
macroDialog.stopMacro();
|
||||
}
|
||||
Telnet3270Client c = client;
|
||||
client = null;
|
||||
new Thread(() -> {
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreen;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreens;
|
||||
import haus.nightmare.lib3270j.macro.MacroXmlParser;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroAction;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Interactive Swing dialog for loading, editing, running, and debugging
|
||||
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
|
||||
*/
|
||||
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextArea xmlArea;
|
||||
private JTextArea logArea;
|
||||
private JLabel statusLabel;
|
||||
private JButton playBtn;
|
||||
private JButton pauseBtn;
|
||||
private JButton stopBtn;
|
||||
|
||||
private Macro macro;
|
||||
|
||||
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
this.macro = new Macro(client);
|
||||
this.macro.addMacroListener(this);
|
||||
this.macro.setPromptHandler(this);
|
||||
|
||||
buildUI();
|
||||
setSize(780, 580);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Top info label
|
||||
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||
topPanel.setOpaque(false);
|
||||
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code><HASCRIPT></code>).<br>"
|
||||
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
|
||||
descLabel.setFont(labelFont);
|
||||
topPanel.add(descLabel, BorderLayout.CENTER);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Center split: XML editor and Execution Log
|
||||
xmlArea = new JTextArea();
|
||||
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
xmlArea.setLineWrap(false);
|
||||
xmlArea.setText(getDefaultMacroXml());
|
||||
ThemeManager.styleTextArea(xmlArea);
|
||||
|
||||
JScrollPane xmlScroll = new JScrollPane(xmlArea);
|
||||
ThemeManager.styleScrollPane(xmlScroll);
|
||||
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
|
||||
|
||||
logArea = new JTextArea();
|
||||
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
|
||||
logArea.setEditable(false);
|
||||
logArea.setLineWrap(true);
|
||||
ThemeManager.styleTextArea(logArea);
|
||||
|
||||
JScrollPane logScroll = new JScrollPane(logArea);
|
||||
ThemeManager.styleScrollPane(logScroll);
|
||||
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
|
||||
|
||||
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
|
||||
splitPane.setResizeWeight(0.65);
|
||||
splitPane.setOpaque(false);
|
||||
mainPanel.add(splitPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom control panel
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton loadBtn = new JButton("Load .mac...");
|
||||
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
loadBtn.setFont(labelFont);
|
||||
loadBtn.addActionListener(e -> loadMacroFile());
|
||||
|
||||
JButton saveBtn = new JButton("Save .mac...");
|
||||
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveBtn.setFont(labelFont);
|
||||
saveBtn.addActionListener(e -> saveMacroFile());
|
||||
|
||||
playBtn = new JButton("Play");
|
||||
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
playBtn.setFont(labelFont);
|
||||
playBtn.addActionListener(e -> playMacro());
|
||||
|
||||
pauseBtn = new JButton("Pause");
|
||||
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pauseBtn.setFont(labelFont);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.addActionListener(e -> togglePause());
|
||||
|
||||
stopBtn = new JButton("Stop");
|
||||
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
stopBtn.setFont(labelFont);
|
||||
stopBtn.setEnabled(false);
|
||||
stopBtn.addActionListener(e -> stopMacro());
|
||||
|
||||
JButton clearLogBtn = new JButton("Clear Log");
|
||||
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
clearLogBtn.setFont(labelFont);
|
||||
clearLogBtn.addActionListener(e -> logArea.setText(""));
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(loadBtn);
|
||||
buttonPanel.add(saveBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(playBtn);
|
||||
buttonPanel.add(pauseBtn);
|
||||
buttonPanel.add(stopBtn);
|
||||
buttonPanel.add(clearLogBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(closeBtn);
|
||||
|
||||
bottomPanel.add(buttonPanel, BorderLayout.EAST);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private void loadMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Open IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
xmlArea.setText(sb.toString());
|
||||
log("Loaded macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Save IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
if (!f.getName().contains(".")) {
|
||||
f = new File(f.getParentFile(), f.getName() + ".mac");
|
||||
}
|
||||
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
|
||||
w.write(xmlArea.getText());
|
||||
log("Saved macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void playMacro() {
|
||||
String xml = xmlArea.getText();
|
||||
if (xml == null || xml.trim().isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
int opt = JOptionPane.showConfirmDialog(this,
|
||||
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
|
||||
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
if (opt != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
MacroScreens screens = MacroXmlParser.parse(xml);
|
||||
macro.setScreens(screens);
|
||||
macro.setClient(client);
|
||||
|
||||
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
|
||||
macro.play();
|
||||
} catch (Exception ex) {
|
||||
log("Parse / Launch Error: " + ex.getMessage());
|
||||
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
public void togglePause() {
|
||||
if (macro.getState() == Macro.STATE_PLAYING) {
|
||||
macro.pause();
|
||||
pauseBtn.setText("Resume");
|
||||
log("Macro paused.");
|
||||
} else if (macro.getState() == Macro.STATE_PAUSED) {
|
||||
macro.resume();
|
||||
pauseBtn.setText("Pause");
|
||||
log("Macro resumed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void stopMacro() {
|
||||
macro.stop();
|
||||
log("Macro stopped by user.");
|
||||
}
|
||||
|
||||
private void log(String msg) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
logArea.append(msg + "\n");
|
||||
logArea.setCaretPosition(logArea.getDocument().getLength());
|
||||
});
|
||||
}
|
||||
|
||||
// ========== MacroListener Callbacks ==========
|
||||
|
||||
@Override
|
||||
public void macroStateChanged(Macro macro, int oldState, int newState) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
switch (newState) {
|
||||
case Macro.STATE_PLAYING:
|
||||
statusLabel.setText("Status: Playing");
|
||||
statusLabel.setForeground(ThemeManager.getFgMain());
|
||||
playBtn.setEnabled(false);
|
||||
pauseBtn.setEnabled(true);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(true);
|
||||
break;
|
||||
case Macro.STATE_PAUSED:
|
||||
statusLabel.setText("Status: Paused");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
pauseBtn.setText("Resume");
|
||||
break;
|
||||
case Macro.STATE_ERROR:
|
||||
statusLabel.setText("Status: Error");
|
||||
statusLabel.setForeground(Color.RED);
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
case Macro.STATE_STOPPED:
|
||||
default:
|
||||
statusLabel.setText("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroScreenMatching(MacroScreen screen) {
|
||||
log("Matched screen: [" + screen.getName() + "]");
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroActionExecuting(MacroAction action) {
|
||||
log(" -> Executing: " + action.getClass().getSimpleName());
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroMessage(String title, String message) {
|
||||
log("Message [" + title + "]: " + message);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroError(String error) {
|
||||
log("ERROR: " + error);
|
||||
}
|
||||
|
||||
// ========== MacroPromptHandler Callback ==========
|
||||
|
||||
@Override
|
||||
public String prompt(MacroActionPrompt prompt) {
|
||||
AtomicReference<String> result = new AtomicReference<>(null);
|
||||
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
result.set(showPromptModal(prompt));
|
||||
} else {
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
result.set(showPromptModal(prompt));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private String showPromptModal(MacroActionPrompt prompt) {
|
||||
JPanel panel = new JPanel(new BorderLayout(5, 5));
|
||||
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
|
||||
panel.add(msgLabel, BorderLayout.NORTH);
|
||||
|
||||
JTextField textField;
|
||||
if (prompt.isEncrypted()) {
|
||||
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
} else {
|
||||
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
}
|
||||
panel.add(textField, BorderLayout.CENTER);
|
||||
|
||||
int opt = JOptionPane.showConfirmDialog(this, panel,
|
||||
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
|
||||
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
|
||||
if (opt == JOptionPane.OK_OPTION) {
|
||||
if (textField instanceof JPasswordField) {
|
||||
return new String(((JPasswordField) textField).getPassword());
|
||||
}
|
||||
return textField.getText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDefaultMacroXml() {
|
||||
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
|
||||
+ " <vars>\n"
|
||||
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
|
||||
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
|
||||
+ " </vars>\n\n"
|
||||
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " <nextscreens timeout=\"10000\">\n"
|
||||
+ " <nextscreen name=\"ReadyScreen\" />\n"
|
||||
+ " </nextscreens>\n"
|
||||
+ " </screen>\n\n"
|
||||
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " </screen>\n"
|
||||
+ "</HASCRIPT>";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user