Initial Commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':lib3270j')
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'org.j3270.J3270App'
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package org.pubvm.j3270;
|
||||
|
||||
import org.lib3270j.*;
|
||||
import org.lib3270j.listener.ConnectionListener;
|
||||
import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import org.pubvm.j3270.ui.ConnectDialog;
|
||||
import org.pubvm.j3270.ui.StatusBar;
|
||||
import org.pubvm.j3270.ui.TerminalPanel;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.*;
|
||||
|
||||
/**
|
||||
* j3270 — Java TN3270 Terminal Emulator.
|
||||
* Main application class. Creates the main window with terminal display,
|
||||
* status bar, menu bar, and manages the Telnet3270Client lifecycle.
|
||||
*/
|
||||
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
|
||||
|
||||
private static final Logger log = Logger.getLogger(J3270App.class.getName());
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
private StatusBar statusBar;
|
||||
private Timer refreshTimer;
|
||||
|
||||
// Last connection details for reconnect
|
||||
private String lastHost = "";
|
||||
private int lastPort = 23;
|
||||
|
||||
public J3270App() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setBackground(new Color(10, 10, 10));
|
||||
|
||||
buildUI();
|
||||
buildMenuBar();
|
||||
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
setMinimumSize(new Dimension(640, 400));
|
||||
|
||||
// Status refresh timer — also ensures focus stays on terminal
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
refreshTimer.start();
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
disconnect();
|
||||
if (refreshTimer != null)
|
||||
refreshTimer.stop();
|
||||
terminalPanel.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
// When window gains focus, push to terminal panel
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
getContentPane().add(statusBar, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void buildMenuBar() {
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
menuBar.setBackground(new Color(30, 30, 30));
|
||||
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
|
||||
|
||||
// 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("Settings...", -1, this::showSettingsDialog));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
||||
disconnect();
|
||||
System.exit(0);
|
||||
}));
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
// 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)));
|
||||
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
|
||||
terminalPanel.setFontSize(16);
|
||||
pack();
|
||||
}));
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
// Actions menu
|
||||
JMenu actionsMenu = createMenu("Actions");
|
||||
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> {
|
||||
if (client != null)
|
||||
client.sendClear();
|
||||
terminalPanel.repaint();
|
||||
}));
|
||||
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
|
||||
if (client != null)
|
||||
client.reset();
|
||||
terminalPanel.repaint();
|
||||
}));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
// Help menu
|
||||
JMenu helpMenu = createMenu("Help");
|
||||
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
|
||||
helpMenu.add(createMenuItem("About", -1, this::showAbout));
|
||||
menuBar.add(helpMenu);
|
||||
|
||||
setJMenuBar(menuBar);
|
||||
}
|
||||
|
||||
private JMenu createMenu(String name) {
|
||||
JMenu menu = new JMenu(name);
|
||||
menu.setForeground(new Color(200, 200, 200));
|
||||
return menu;
|
||||
}
|
||||
|
||||
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) {
|
||||
JMenuItem item = new JMenuItem(text);
|
||||
item.setBackground(new Color(40, 40, 40));
|
||||
item.setForeground(new Color(200, 200, 200));
|
||||
if (acceleratorKey > 0) {
|
||||
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey,
|
||||
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
|
||||
}
|
||||
item.addActionListener(e -> action.run());
|
||||
return item;
|
||||
}
|
||||
|
||||
// ========== Connection management ==========
|
||||
|
||||
private void showConnectDialog() {
|
||||
ConnectDialog dialog = new ConnectDialog(this);
|
||||
dialog.setInitialHost(lastHost);
|
||||
dialog.setInitialPort(lastPort);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed()) {
|
||||
ConnectionConfig config = dialog.getConnectionConfig();
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
connect(config);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSettingsDialog() {
|
||||
org.pubvm.j3270.ui.SettingsDialog dialog = new org.pubvm.j3270.ui.SettingsDialog(this);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
public TerminalPanel getTerminalPanel() {
|
||||
return terminalPanel;
|
||||
}
|
||||
|
||||
void connect(ConnectionConfig config) {
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
|
||||
// Save for auto-connect
|
||||
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||
|
||||
// Disconnect existing connection
|
||||
disconnect();
|
||||
|
||||
client = new Telnet3270Client(config);
|
||||
client.addConnectionListener(this);
|
||||
client.addScreenUpdateListener(this);
|
||||
|
||||
terminalPanel.setClient(client);
|
||||
statusBar.setClient(client);
|
||||
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
|
||||
|
||||
// Resize window to match model
|
||||
terminalPanel.revalidate();
|
||||
pack();
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.connect();
|
||||
} catch (IOException e) {
|
||||
log.log(Level.WARNING, "Connection failed", e);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Connection failed: " + e.getMessage(),
|
||||
"Connection Error", JOptionPane.ERROR_MESSAGE);
|
||||
});
|
||||
}
|
||||
}, "Connect-Thread").start();
|
||||
|
||||
// Focus the terminal panel
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
if (client != null) {
|
||||
client.disconnect();
|
||||
client = null;
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null);
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustFontSize(int delta) {
|
||||
int current = terminalPanel.getFontSize();
|
||||
int newSize = Math.max(8, Math.min(36, current + delta));
|
||||
terminalPanel.setFontSize(newSize);
|
||||
pack();
|
||||
}
|
||||
|
||||
// ========== ConnectionListener ==========
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
log.info("Connection state: " + oldState + " -> " + newState);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusBar.updateStatus();
|
||||
terminalPanel.repaint();
|
||||
|
||||
// Auto-resize and focus on first full session
|
||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||
terminalPanel.revalidate();
|
||||
pack();
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {
|
||||
log.warning("Connection error: " + message);
|
||||
SwingUtilities.invokeLater(statusBar::updateStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTN3270ENegotiated(String deviceType, String deviceName) {
|
||||
log.info("TN3270E negotiated: type=" + deviceType + " name=" + deviceName);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (deviceName != null) {
|
||||
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== ScreenUpdateListener ==========
|
||||
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (client != null) {
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoundAlarm() {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
terminalPanel.revalidate();
|
||||
pack();
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Help dialogs ==========
|
||||
|
||||
private void showKeyMappings() {
|
||||
String text = "Key Mappings:\n\n" +
|
||||
"Enter — Send Enter (AID)\n" +
|
||||
"F1-F12 — PF1-PF12\n" +
|
||||
"Shift+F1-F12 — PF13-PF24\n" +
|
||||
"Alt+1/2/3 — PA1/PA2/PA3\n" +
|
||||
"Tab — Tab to next field\n" +
|
||||
"Shift+Tab — Back-tab\n" +
|
||||
"Arrow keys — Cursor movement\n" +
|
||||
"Home — Cursor to home\n" +
|
||||
"End — Erase to end of field\n" +
|
||||
"Delete — Delete character\n" +
|
||||
"Backspace — Backspace\n" +
|
||||
"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";
|
||||
|
||||
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(360, 400));
|
||||
JOptionPane.showMessageDialog(this, sp, "Key Mappings", 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",
|
||||
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = false;
|
||||
String configFile = null;
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
|
||||
debug = true;
|
||||
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||
configFile = args[++i];
|
||||
} else {
|
||||
remainingArgs.add(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure logging over the absolute root logger
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
}
|
||||
globalRoot.setLevel(debug ? Level.ALL : Level.WARNING);
|
||||
ConsoleHandler handler = new ConsoleHandler();
|
||||
handler.setLevel(Level.ALL);
|
||||
handler.setFormatter(new SimpleFormatter());
|
||||
globalRoot.addHandler(handler);
|
||||
|
||||
// Load INI config file if specified
|
||||
if (configFile != null) {
|
||||
try {
|
||||
org.pubvm.j3270.config.Settings.loadFromIniFile(configFile);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to load config file: " + configFile);
|
||||
System.err.println(e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
J3270App app = new J3270App();
|
||||
app.setVisible(true);
|
||||
|
||||
// If host:port given on command line, connect directly
|
||||
if (!remainingArgs.isEmpty()) {
|
||||
String host = remainingArgs.get(0);
|
||||
int port = 23;
|
||||
if (remainingArgs.size() >= 2) {
|
||||
try {
|
||||
port = Integer.parseInt(remainingArgs.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
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 e) {
|
||||
}
|
||||
}
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, model);
|
||||
app.connect(config);
|
||||
} else {
|
||||
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
|
||||
.getStartupBehavior();
|
||||
switch (behavior) {
|
||||
case SHOW_CONNECT:
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
break;
|
||||
case AUTO_CONNECT:
|
||||
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
||||
if (host != null && !host.isEmpty()) {
|
||||
app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4));
|
||||
} else {
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
}
|
||||
break;
|
||||
case DO_NOTHING:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package org.pubvm.j3270.config;
|
||||
|
||||
import java.util.prefs.Preferences;
|
||||
import java.awt.Color;
|
||||
import java.io.*;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class Settings {
|
||||
private static final Preferences prefs = Preferences.userNodeForPackage(Settings.class);
|
||||
private static final Logger log = Logger.getLogger(Settings.class.getName());
|
||||
|
||||
public enum StartupBehavior {
|
||||
SHOW_CONNECT,
|
||||
DO_NOTHING,
|
||||
AUTO_CONNECT
|
||||
}
|
||||
|
||||
public static String getFontFamily() {
|
||||
return prefs.get("fontFamily", "Monospaced");
|
||||
}
|
||||
|
||||
public static void setFontFamily(String family) {
|
||||
prefs.put("fontFamily", family);
|
||||
}
|
||||
|
||||
public static int getFontSize() {
|
||||
return prefs.getInt("fontSize", 16);
|
||||
}
|
||||
|
||||
public static void setFontSize(int size) {
|
||||
prefs.putInt("fontSize", size);
|
||||
}
|
||||
|
||||
public static StartupBehavior getStartupBehavior() {
|
||||
String behaviorStr = prefs.get("startupBehavior", StartupBehavior.SHOW_CONNECT.name());
|
||||
try {
|
||||
return StartupBehavior.valueOf(behaviorStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return StartupBehavior.SHOW_CONNECT;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setStartupBehavior(StartupBehavior behavior) {
|
||||
prefs.put("startupBehavior", behavior.name());
|
||||
}
|
||||
|
||||
public static String getAutoConnectHost() {
|
||||
return prefs.get("autoConnectHost", "");
|
||||
}
|
||||
|
||||
public static void setAutoConnectHost(String host) {
|
||||
prefs.put("autoConnectHost", host);
|
||||
}
|
||||
|
||||
public static int getAutoConnectPort() {
|
||||
return prefs.getInt("autoConnectPort", 23);
|
||||
}
|
||||
|
||||
public static void setAutoConnectPort(int port) {
|
||||
prefs.putInt("autoConnectPort", port);
|
||||
}
|
||||
|
||||
public static Color getColorOverride(int index, Color defaultColor) {
|
||||
String hex = prefs.get("color_" + index, null);
|
||||
try {
|
||||
return hex != null ? Color.decode(hex) : defaultColor;
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setColorOverride(int index, Color color) {
|
||||
prefs.put("color_" + index, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
|
||||
}
|
||||
|
||||
public static Color getMonoColorOverride(String key, Color defaultColor) {
|
||||
String hex = prefs.get("mono_" + key, null);
|
||||
try {
|
||||
return hex != null ? Color.decode(hex) : defaultColor;
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setMonoColorOverride(String key, Color color) {
|
||||
prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
|
||||
}
|
||||
|
||||
public static String getKeyBinding(String action, String defaultBinding) {
|
||||
return prefs.get("key_" + action, defaultBinding);
|
||||
}
|
||||
|
||||
public static void setKeyBinding(String action, String binding) {
|
||||
prefs.put("key_" + action, binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from an INI-style configuration file.
|
||||
* Sections: [appearance], [behavior], [colors], [keybindings]
|
||||
*
|
||||
* Example file:
|
||||
* <pre>
|
||||
* [appearance]
|
||||
* fontFamily = IBM 3270
|
||||
* fontSize = 18
|
||||
*
|
||||
* [behavior]
|
||||
* startupBehavior = AUTO_CONNECT
|
||||
* autoConnectHost = mainframe.example.com
|
||||
* autoConnectPort = 23
|
||||
*
|
||||
* [colors]
|
||||
* ; Host colors 0-15
|
||||
* color_0 = #000000
|
||||
* color_4 = #00ff00
|
||||
* ; Monochrome / background
|
||||
* mono_NORMAL = #32cd32
|
||||
* mono_BACKGROUND = #0a0a0a
|
||||
*
|
||||
* [keybindings]
|
||||
* ; Comma-separated for multiple bindings
|
||||
* PF12 = F12, shift F12
|
||||
* PF24 = UNBOUND
|
||||
* CLEAR = alt C
|
||||
* </pre>
|
||||
*/
|
||||
public static void loadFromIniFile(String path) throws IOException {
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("Config file not found: " + path);
|
||||
}
|
||||
|
||||
log.info("Loading configuration from: " + path);
|
||||
String currentSection = "";
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
int lineNum = 0;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
lineNum++;
|
||||
line = line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Section header
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
currentSection = line.substring(1, line.length() - 1).toLowerCase().trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key = Value
|
||||
int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
log.warning("Config line " + lineNum + ": no '=' found, skipping: " + line);
|
||||
continue;
|
||||
}
|
||||
String key = line.substring(0, eq).trim();
|
||||
String value = line.substring(eq + 1).trim();
|
||||
|
||||
try {
|
||||
applyConfigEntry(currentSection, key, value);
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Config line " + lineNum + ": error applying " + key + "=" + value, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Configuration loaded successfully.");
|
||||
}
|
||||
|
||||
private static void applyConfigEntry(String section, String key, String value) {
|
||||
switch (section) {
|
||||
case "appearance":
|
||||
switch (key) {
|
||||
case "fontFamily": setFontFamily(value); break;
|
||||
case "fontSize": setFontSize(Integer.parseInt(value)); break;
|
||||
default:
|
||||
log.warning("Unknown appearance key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "behavior":
|
||||
switch (key) {
|
||||
case "startupBehavior":
|
||||
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "autoConnectHost": setAutoConnectHost(value); break;
|
||||
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
|
||||
default:
|
||||
log.warning("Unknown behavior key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "colors":
|
||||
if (key.startsWith("color_")) {
|
||||
int index = Integer.parseInt(key.substring(6));
|
||||
setColorOverride(index, Color.decode(value));
|
||||
} else if (key.startsWith("mono_")) {
|
||||
String monoKey = key.substring(5);
|
||||
setMonoColorOverride(monoKey, Color.decode(value));
|
||||
} else {
|
||||
log.warning("Unknown colors key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "keybindings":
|
||||
setKeyBinding(key, value);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Allow bare keys outside any section — treat as raw prefs
|
||||
log.fine("Setting raw preference: " + key + " = " + value);
|
||||
prefs.put(key, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
import org.lib3270j.TerminalModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Connection dialog for entering host, port, model, and LU name.
|
||||
*/
|
||||
public class ConnectDialog extends JDialog {
|
||||
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JComboBox<TerminalModel> modelCombo;
|
||||
private JTextField luField;
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
public ConnectDialog(Frame parent) {
|
||||
super(parent, "Connect to Host", true);
|
||||
buildUI();
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new GridBagLayout());
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
mainPanel.setBackground(new Color(30, 30, 30));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14);
|
||||
Color fg = new Color(200, 200, 200);
|
||||
|
||||
// Host
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
hostLabel.setForeground(fg);
|
||||
hostLabel.setFont(labelFont);
|
||||
mainPanel.add(hostLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
hostField = createDarkField(20);
|
||||
mainPanel.add(hostField, gbc);
|
||||
|
||||
// Port
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.weightx = 0;
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
portLabel.setForeground(fg);
|
||||
portLabel.setFont(labelFont);
|
||||
mainPanel.add(portLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
portField = createDarkField(6);
|
||||
portField.setText("23");
|
||||
mainPanel.add(portField, gbc);
|
||||
|
||||
// Model
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.weightx = 0;
|
||||
JLabel modelLabel = new JLabel("Model:");
|
||||
modelLabel.setForeground(fg);
|
||||
modelLabel.setFont(labelFont);
|
||||
mainPanel.add(modelLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
modelCombo = new JComboBox<>(TerminalModel.values());
|
||||
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
|
||||
modelCombo.setBackground(new Color(45, 45, 45));
|
||||
modelCombo.setForeground(fg);
|
||||
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
mainPanel.add(modelCombo, gbc);
|
||||
|
||||
// LU Name
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 3;
|
||||
gbc.weightx = 0;
|
||||
JLabel luLabel = new JLabel("LU Name:");
|
||||
luLabel.setForeground(fg);
|
||||
luLabel.setFont(labelFont);
|
||||
mainPanel.add(luLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
luField = createDarkField(12);
|
||||
mainPanel.add(luField, gbc);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||
|
||||
JButton connectBtn = new JButton("Connect");
|
||||
connectBtn.setBackground(new Color(50, 120, 50));
|
||||
connectBtn.setForeground(Color.WHITE);
|
||||
connectBtn.setFont(labelFont);
|
||||
connectBtn.addActionListener(e -> onConnect());
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel");
|
||||
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||
cancelBtn.setForeground(fg);
|
||||
cancelBtn.setFont(labelFont);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
confirmed = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(connectBtn);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
|
||||
// Enter key triggers connect
|
||||
getRootPane().setDefaultButton(connectBtn);
|
||||
}
|
||||
|
||||
private JTextField createDarkField(int cols) {
|
||||
JTextField field = new JTextField(cols);
|
||||
field.setBackground(new Color(45, 45, 45));
|
||||
field.setForeground(new Color(200, 200, 200));
|
||||
field.setCaretColor(new Color(200, 200, 200));
|
||||
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
|
||||
field.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
|
||||
return field;
|
||||
}
|
||||
|
||||
private void onConnect() {
|
||||
String host = hostField.getText().trim();
|
||||
if (host.isEmpty()) {
|
||||
hostField.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = Integer.parseInt(portField.getText().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
portField.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem());
|
||||
String lu = luField.getText().trim();
|
||||
if (!lu.isEmpty()) {
|
||||
result.setLuName(lu);
|
||||
}
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
public ConnectionConfig getConnectionConfig() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Set initial values. */
|
||||
public void setInitialHost(String host) {
|
||||
hostField.setText(host);
|
||||
}
|
||||
|
||||
public void setInitialPort(int port) {
|
||||
portField.setText(String.valueOf(port));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import org.pubvm.j3270.J3270App;
|
||||
import org.pubvm.j3270.config.Settings;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.border.LineBorder;
|
||||
|
||||
public class SettingsDialog extends JDialog {
|
||||
|
||||
private final J3270App parentApp;
|
||||
|
||||
// Appearance tab
|
||||
private JComboBox<String> fontBox;
|
||||
private JSpinner fontSizeSpinner;
|
||||
|
||||
// Behavior tab
|
||||
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
||||
private JPanel autoConnectPanel;
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
|
||||
// Advanced tab state tracking
|
||||
private final Color[] tempHostColors = new Color[16];
|
||||
private final Map<String, Color> tempMonoColors = new HashMap<>();
|
||||
private final Map<String, String> tempKeyBindings = new HashMap<>();
|
||||
private DefaultTableModel keymapModel;
|
||||
|
||||
public SettingsDialog(J3270App parent) {
|
||||
super(parent, "Settings", true);
|
||||
this.parentApp = parent;
|
||||
|
||||
initComponents();
|
||||
setSize(550, 450);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
JTabbedPane tabbedPane = new JTabbedPane();
|
||||
|
||||
tabbedPane.addTab("Appearance", createAppearancePanel());
|
||||
tabbedPane.addTab("Behavior", createBehaviorPanel());
|
||||
tabbedPane.addTab("Advanced", createAdvancedPanel());
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton btnOk = new JButton("OK");
|
||||
JButton btnApply = new JButton("Apply");
|
||||
JButton btnCancel = new JButton("Cancel");
|
||||
|
||||
btnOk.addActionListener((ActionEvent e) -> {
|
||||
boolean success = applySettings();
|
||||
if (success) {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
btnApply.addActionListener((ActionEvent e) -> {
|
||||
applySettings();
|
||||
});
|
||||
|
||||
btnCancel.addActionListener((ActionEvent e) -> {
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(btnApply);
|
||||
buttonPanel.add(btnCancel);
|
||||
buttonPanel.add(btnOk);
|
||||
|
||||
getContentPane().add(tabbedPane, BorderLayout.CENTER);
|
||||
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private JPanel createAppearancePanel() {
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
// Font family
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel fontLabel = new JLabel("Terminal Font:");
|
||||
panel.add(fontLabel, gbc);
|
||||
|
||||
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
|
||||
fontBox = new JComboBox<>(fonts);
|
||||
// Find default
|
||||
String currentFont = Settings.getFontFamily();
|
||||
for (int i = 0; i < fonts.length; i++) {
|
||||
if (fonts[i].equalsIgnoreCase(currentFont)) {
|
||||
fontBox.setSelectedIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(fontBox, gbc);
|
||||
|
||||
// Font size
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel sizeLabel = new JLabel("Font Size:");
|
||||
panel.add(sizeLabel, gbc);
|
||||
|
||||
SpinnerNumberModel sizeModel = new SpinnerNumberModel(Settings.getFontSize(), 8, 72, 1);
|
||||
fontSizeSpinner = new JSpinner(sizeModel);
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(fontSizeSpinner, gbc);
|
||||
|
||||
// Fill remaining space
|
||||
gbc.gridy = 2;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createBehaviorPanel() {
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel actionLabel = new JLabel("Startup Action:");
|
||||
panel.add(actionLabel, gbc);
|
||||
|
||||
startupBehaviorBox = new JComboBox<>(Settings.StartupBehavior.values());
|
||||
startupBehaviorBox.setSelectedItem(Settings.getStartupBehavior());
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(startupBehaviorBox, gbc);
|
||||
|
||||
// Auto-connect panel
|
||||
autoConnectPanel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints acGbc = new GridBagConstraints();
|
||||
acGbc.insets = new Insets(5, 5, 5, 5);
|
||||
acGbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
acGbc.gridx = 0;
|
||||
acGbc.gridy = 0;
|
||||
autoConnectPanel.add(hostLabel, acGbc);
|
||||
|
||||
hostField = new JTextField(Settings.getAutoConnectHost(), 15);
|
||||
acGbc.gridx = 1;
|
||||
autoConnectPanel.add(hostField, acGbc);
|
||||
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
acGbc.gridx = 2;
|
||||
autoConnectPanel.add(portLabel, acGbc);
|
||||
|
||||
portField = new JTextField(String.valueOf(Settings.getAutoConnectPort()), 4);
|
||||
acGbc.gridx = 3;
|
||||
autoConnectPanel.add(portField, acGbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.gridwidth = 2;
|
||||
panel.add(autoConnectPanel, gbc);
|
||||
|
||||
// Toggle visibility
|
||||
startupBehaviorBox.addActionListener(e -> {
|
||||
autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT);
|
||||
});
|
||||
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
|
||||
|
||||
// Placeholder for potentially more behavior options below
|
||||
gbc.gridy = 2;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createAdvancedPanel() {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
|
||||
JTabbedPane advancedTabs = new JTabbedPane();
|
||||
advancedTabs.addTab("Colors", createColorsPanel());
|
||||
advancedTabs.addTab("Keymap", createKeymapPanel());
|
||||
|
||||
panel.add(advancedTabs, BorderLayout.CENTER);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createColorsPanel() {
|
||||
JPanel main = new JPanel(new BorderLayout());
|
||||
|
||||
// Host colors grid (16 colors)
|
||||
JPanel hostPanel = new JPanel(new GridLayout(2, 8, 5, 5));
|
||||
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
|
||||
tempHostColors[i] = init;
|
||||
JPanel cb = new JPanel();
|
||||
cb.setBackground(init);
|
||||
cb.setPreferredSize(new Dimension(30, 30));
|
||||
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
||||
|
||||
final int index = i;
|
||||
cb.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
Color c = JColorChooser.showDialog(main, "Select Color " + index, tempHostColors[index]);
|
||||
if (c != null) {
|
||||
tempHostColors[index] = c;
|
||||
cb.setBackground(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
hostPanel.add(cb);
|
||||
}
|
||||
|
||||
// Mono colors
|
||||
JPanel monoPanel = new JPanel(new GridLayout(2, 3, 5, 5));
|
||||
monoPanel.setBorder(BorderFactory.createTitledBorder("Monochrome / BG Colors"));
|
||||
|
||||
String[] monoKeys = {"NORMAL", "INTENSIFY", "PROTECTED", "PROTECTED_HIGH", "BACKGROUND"};
|
||||
Color[] monoDefs = {TerminalPanel.DEFAULT_MONO_NORMAL, TerminalPanel.DEFAULT_MONO_INTENSIFY,
|
||||
TerminalPanel.DEFAULT_MONO_PROTECTED, TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH, TerminalPanel.DEFAULT_BG_COLOR};
|
||||
|
||||
for (int i = 0; i < monoKeys.length; i++) {
|
||||
String mk = monoKeys[i];
|
||||
Color init = org.pubvm.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
|
||||
tempMonoColors.put(mk, init);
|
||||
|
||||
JPanel p = new JPanel(new BorderLayout());
|
||||
p.add(new JLabel(mk, SwingConstants.CENTER), BorderLayout.NORTH);
|
||||
JPanel cb = new JPanel();
|
||||
cb.setBackground(init);
|
||||
cb.setPreferredSize(new Dimension(40, 40));
|
||||
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
||||
|
||||
cb.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
Color c = JColorChooser.showDialog(main, "Select " + mk, tempMonoColors.get(mk));
|
||||
if (c != null) {
|
||||
tempMonoColors.put(mk, c);
|
||||
cb.setBackground(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
p.add(cb, BorderLayout.CENTER);
|
||||
monoPanel.add(p);
|
||||
}
|
||||
|
||||
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton btnResetColors = new JButton("Reset to Defaults");
|
||||
btnResetColors.addActionListener(e -> {
|
||||
for (int i=0; i<16; i++) {
|
||||
tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i];
|
||||
}
|
||||
for (int i=0; i<monoKeys.length; i++) {
|
||||
tempMonoColors.put(monoKeys[i], monoDefs[i]);
|
||||
}
|
||||
// Repaint container implicitly handled if we trigger a UI update,
|
||||
// but for simplicity user can just close/reopen or have it refresh on save
|
||||
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
|
||||
});
|
||||
resetPanel.add(btnResetColors);
|
||||
|
||||
main.add(hostPanel, BorderLayout.NORTH);
|
||||
main.add(monoPanel, BorderLayout.CENTER);
|
||||
main.add(resetPanel, BorderLayout.SOUTH);
|
||||
|
||||
return main;
|
||||
}
|
||||
|
||||
private JPanel createKeymapPanel() {
|
||||
JPanel main = new JPanel(new BorderLayout());
|
||||
|
||||
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
|
||||
|
||||
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
|
||||
@Override
|
||||
public boolean isCellEditable(int row, int column) {
|
||||
return false; // read only through UI editor button
|
||||
}
|
||||
};
|
||||
|
||||
// Populate table from Settings or Defaults
|
||||
for(String act : actions) {
|
||||
String def = act;
|
||||
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
|
||||
String current = org.pubvm.j3270.config.Settings.getKeyBinding(act, def);
|
||||
tempKeyBindings.put(act, current);
|
||||
keymapModel.addRow(new Object[]{act, current});
|
||||
}
|
||||
for(int i=1;i<=24;i++){
|
||||
String defaultBinding;
|
||||
if (i <= 12) {
|
||||
defaultBinding = "F" + i;
|
||||
} else {
|
||||
defaultBinding = "shift F" + (i - 12);
|
||||
}
|
||||
String pf = "PF"+i;
|
||||
String cur = org.pubvm.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
|
||||
tempKeyBindings.put(pf, cur);
|
||||
keymapModel.addRow(new Object[]{pf, cur});
|
||||
}
|
||||
for(int i=1;i<=3;i++){
|
||||
String pa = "PA"+i;
|
||||
String curPa = org.pubvm.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
|
||||
tempKeyBindings.put(pa, curPa);
|
||||
keymapModel.addRow(new Object[]{pa, curPa});
|
||||
}
|
||||
|
||||
JTable table = new JTable(keymapModel);
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setFillsViewportHeight(true);
|
||||
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4));
|
||||
|
||||
// Helper to open a key-capture dialog and return the keystroke string
|
||||
// The callback receives the captured string.
|
||||
java.util.function.BiConsumer<String, java.util.function.Consumer<String>> captureKey = (title, callback) -> {
|
||||
JDialog captureDialog = new JDialog(this, title, true);
|
||||
captureDialog.setSize(320, 100);
|
||||
captureDialog.setLocationRelativeTo(this);
|
||||
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
|
||||
captureDialog.add(lbl);
|
||||
captureDialog.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
int code = e.getKeyCode();
|
||||
if (code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_CONTROL ||
|
||||
code == KeyEvent.VK_ALT || code == KeyEvent.VK_META) return;
|
||||
KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e);
|
||||
if (ks != null) {
|
||||
String s = ks.toString().replace("pressed ", "");
|
||||
callback.accept(s);
|
||||
captureDialog.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
captureDialog.setVisible(true);
|
||||
};
|
||||
|
||||
// Replace Binding — sets the action to exactly one new key
|
||||
JButton btnReplace = new JButton("Replace Binding");
|
||||
btnReplace.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
String action = (String) keymapModel.getValueAt(row, 0);
|
||||
captureKey.accept("Press key for " + action, s -> {
|
||||
tempKeyBindings.put(action, s);
|
||||
keymapModel.setValueAt(s, row, 1);
|
||||
});
|
||||
});
|
||||
btnPanel.add(btnReplace);
|
||||
|
||||
// Add Binding — appends an additional key to the existing binding(s)
|
||||
JButton btnAdd = new JButton("Add Binding");
|
||||
btnAdd.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
String action = (String) keymapModel.getValueAt(row, 0);
|
||||
captureKey.accept("Press additional key for " + action, s -> {
|
||||
String existing = tempKeyBindings.get(action);
|
||||
String updated;
|
||||
if (existing == null || existing.isEmpty() || "UNBOUND".equals(existing)) {
|
||||
updated = s;
|
||||
} else {
|
||||
updated = existing + ", " + s;
|
||||
}
|
||||
tempKeyBindings.put(action, updated);
|
||||
keymapModel.setValueAt(updated, row, 1);
|
||||
});
|
||||
});
|
||||
btnPanel.add(btnAdd);
|
||||
|
||||
// Remove Last — removes the last comma-separated binding entry
|
||||
JButton btnRemoveLast = new JButton("Remove Last");
|
||||
btnRemoveLast.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
String action = (String) keymapModel.getValueAt(row, 0);
|
||||
String existing = tempKeyBindings.get(action);
|
||||
if (existing == null || "UNBOUND".equals(existing)) return;
|
||||
int lastComma = existing.lastIndexOf(',');
|
||||
String updated;
|
||||
if (lastComma > 0) {
|
||||
updated = existing.substring(0, lastComma).trim();
|
||||
} else {
|
||||
updated = "UNBOUND";
|
||||
}
|
||||
tempKeyBindings.put(action, updated);
|
||||
keymapModel.setValueAt(updated, row, 1);
|
||||
});
|
||||
btnPanel.add(btnRemoveLast);
|
||||
|
||||
// Unbind All — clears all bindings for the action
|
||||
JButton btnUnbind = new JButton("Unbind All");
|
||||
btnUnbind.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
String action = (String) keymapModel.getValueAt(row, 0);
|
||||
tempKeyBindings.put(action, "UNBOUND");
|
||||
keymapModel.setValueAt("UNBOUND", row, 1);
|
||||
});
|
||||
btnPanel.add(btnUnbind);
|
||||
|
||||
// Reset Keymaps — restore all defaults
|
||||
JButton btnReset = new JButton("Reset All");
|
||||
btnReset.addActionListener(e -> {
|
||||
for (int row = 0; row < keymapModel.getRowCount(); row++) {
|
||||
String action = (String) keymapModel.getValueAt(row, 0);
|
||||
String def = getDefaultBinding(action);
|
||||
tempKeyBindings.put(action, def);
|
||||
keymapModel.setValueAt(def, row, 1);
|
||||
}
|
||||
});
|
||||
btnPanel.add(btnReset);
|
||||
|
||||
main.add(new JScrollPane(table), BorderLayout.CENTER);
|
||||
main.add(btnPanel, BorderLayout.SOUTH);
|
||||
|
||||
return main;
|
||||
}
|
||||
|
||||
/** Returns the factory-default binding for a given action name. */
|
||||
private String getDefaultBinding(String action) {
|
||||
if (action.startsWith("PF")) {
|
||||
int n = Integer.parseInt(action.substring(2));
|
||||
return n <= 12 ? "F" + n : "shift F" + (n - 12);
|
||||
}
|
||||
if (action.startsWith("PA")) return "alt " + action.substring(2);
|
||||
if (action.equals("CLEAR")) return "alt C";
|
||||
return action; // nav keys default to their own name
|
||||
}
|
||||
|
||||
private boolean applySettings() {
|
||||
try {
|
||||
// Apply Appearance
|
||||
String fontFam = (String) fontBox.getSelectedItem();
|
||||
if (fontFam != null) {
|
||||
Settings.setFontFamily(fontFam);
|
||||
}
|
||||
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
|
||||
|
||||
// Apply Behavior
|
||||
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
|
||||
if (behavior != null) {
|
||||
Settings.setStartupBehavior(behavior);
|
||||
}
|
||||
if (behavior == Settings.StartupBehavior.AUTO_CONNECT) {
|
||||
Settings.setAutoConnectHost(hostField.getText().trim());
|
||||
try {
|
||||
Settings.setAutoConnectPort(Integer.parseInt(portField.getText().trim()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate visual changes to the app
|
||||
// Save Colors
|
||||
for (int i=0; i<16; i++) {
|
||||
org.pubvm.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
|
||||
}
|
||||
for (Map.Entry<String, Color> entry : tempMonoColors.entrySet()) {
|
||||
org.pubvm.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// Save Keybindings
|
||||
for (Map.Entry<String, String> entry : tempKeyBindings.entrySet()) {
|
||||
org.pubvm.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
parentApp.getTerminalPanel().reloadSettings();
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to completely apply settings: " + e.getMessage(), "Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
|
||||
import org.lib3270j.ConnectionState;
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 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 cursorPosition;
|
||||
private final JLabel luName;
|
||||
private final JLabel lockStatus;
|
||||
private final JLabel modelInfo;
|
||||
|
||||
private Telnet3270Client client;
|
||||
|
||||
// OIA colors
|
||||
private static final Color OIA_BG = new Color(20, 20, 20);
|
||||
private static final Color OIA_FG = new Color(50, 205, 50);
|
||||
private static final Color OIA_DIM = new Color(80, 80, 80);
|
||||
private static final Color OIA_ALERT = new Color(255, 80, 80);
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
setBackground(OIA_BG);
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
|
||||
setPreferredSize(new Dimension(800, 22));
|
||||
|
||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
|
||||
luName = createLabel("", oiaFont, OIA_FG);
|
||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
|
||||
|
||||
add(Box.createHorizontalStrut(6));
|
||||
add(connectionStatus);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(luName);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lockStatus);
|
||||
add(Box.createHorizontalGlue());
|
||||
add(modelInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(cursorPosition);
|
||||
add(Box.createHorizontalStrut(6));
|
||||
}
|
||||
|
||||
private JLabel createLabel(String text, Font font, Color fg) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.setFont(font);
|
||||
label.setForeground(fg);
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
// Connection state
|
||||
ConnectionState state = client.getConnectionState();
|
||||
switch (state) {
|
||||
case NOT_CONNECTED:
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
break;
|
||||
case TCP_PENDING:
|
||||
case TELNET_PENDING:
|
||||
connectionStatus.setText("Connecting...");
|
||||
connectionStatus.setForeground(OIA_ALERT);
|
||||
break;
|
||||
case CONNECTED_3270:
|
||||
connectionStatus.setText("TN3270");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_TN3270E:
|
||||
connectionStatus.setText("TN3270E");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_SSCP:
|
||||
connectionStatus.setText("SSCP-LU");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_NVT:
|
||||
case CONNECTED_NVT_CHAR:
|
||||
case CONNECTED_E_NVT:
|
||||
connectionStatus.setText("NVT");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_UNBOUND:
|
||||
connectionStatus.setText("Unbound");
|
||||
connectionStatus.setForeground(new Color(255, 255, 80));
|
||||
break;
|
||||
default:
|
||||
connectionStatus.setText(state.name());
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
break;
|
||||
}
|
||||
|
||||
// LU name
|
||||
String lu = "";
|
||||
if (client.getConnectionState().isTn3270e()) {
|
||||
// lu would come from FSM
|
||||
}
|
||||
luName.setText(lu);
|
||||
|
||||
// Lock status
|
||||
if (client.getInputProcessor().isKeyboardLocked()) {
|
||||
lockStatus.setText("X SYSTEM");
|
||||
lockStatus.setForeground(OIA_ALERT);
|
||||
} else if (client.getInputProcessor().isInsertMode()) {
|
||||
lockStatus.setText("INSERT");
|
||||
lockStatus.setForeground(OIA_FG);
|
||||
} else {
|
||||
lockStatus.setText("");
|
||||
}
|
||||
|
||||
// Model info
|
||||
modelInfo.setText(client.getConfig().getModel().getTerminalType());
|
||||
|
||||
// Cursor position
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int row = sb.getCursorRow() + 1;
|
||||
int col = sb.getCursorCol() + 1;
|
||||
cursorPosition.setText(String.format("%03d/%03d", row, col));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Custom JPanel that renders the 3270 screen buffer.
|
||||
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
|
||||
* attributes.
|
||||
*/
|
||||
public class TerminalPanel extends JPanel {
|
||||
|
||||
private Telnet3270Client client;
|
||||
|
||||
// Font and cell dimensions
|
||||
private Font terminalFont;
|
||||
private int cellWidth;
|
||||
private int cellHeight;
|
||||
private int fontAscent;
|
||||
private int fontDescent;
|
||||
private int padding = 4;
|
||||
private int currentFontSize = 16;
|
||||
|
||||
// Cursor state
|
||||
private boolean cursorVisible = true;
|
||||
private Timer blinkTimer;
|
||||
|
||||
// Host color mapping
|
||||
private final Color[] hostColors = new Color[16];
|
||||
|
||||
// 3278 monochrome colors (based on field attributes)
|
||||
private Color monoNormal;
|
||||
private Color monoIntensify;
|
||||
private Color monoProtected;
|
||||
private Color monoProtectedHigh;
|
||||
|
||||
// Background
|
||||
private Color bgColor;
|
||||
|
||||
// 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(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(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(180, 130, 255), // 11: Purple
|
||||
new Color(144, 238, 144), // 12: Pale Green
|
||||
new Color(175, 238, 238), // 13: Pale Turquoise
|
||||
new Color(170, 170, 170), // 14: Grey
|
||||
new Color(255, 255, 255), // 15: White
|
||||
};
|
||||
|
||||
// Default 3278 monochrome colors
|
||||
public static final Color DEFAULT_MONO_NORMAL = new Color(50, 205, 50);
|
||||
public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 255, 255);
|
||||
public static final Color DEFAULT_MONO_PROTECTED = new Color(80, 120, 255);
|
||||
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
|
||||
|
||||
// Default Background
|
||||
public static final Color DEFAULT_BG_COLOR = new Color(10, 10, 10);
|
||||
|
||||
public TerminalPanel() {
|
||||
setupColors();
|
||||
setBackground(bgColor);
|
||||
setFocusable(true);
|
||||
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();
|
||||
setupCursorBlink();
|
||||
|
||||
// Handle mouse clicks to position cursor and grab focus
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
requestFocusInWindow();
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
int col = (e.getX() - padding) / cellWidth;
|
||||
int row = (e.getY() - padding) / cellHeight;
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
if (row >= 0 && row < sb.getRows() && col >= 0 && col < sb.getCols()) {
|
||||
sb.setCursorAddress(row * sb.getCols() + col);
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if (!binding.isEmpty() && !"UNBOUND".equals(binding)) {
|
||||
KeyStroke ks = KeyStroke.getKeyStroke(binding);
|
||||
if (ks != null) {
|
||||
im.put(ks, "j3270-" + action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupKeyBindings() {
|
||||
InputMap im = getInputMap(JComponent.WHEN_FOCUSED);
|
||||
ActionMap am = getActionMap();
|
||||
|
||||
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" };
|
||||
for (String key : navKeys) {
|
||||
String binding = org.pubvm.j3270.config.Settings.getKeyBinding(key, key);
|
||||
bindKeyToMap(im, key, binding);
|
||||
}
|
||||
|
||||
// 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 binding = org.pubvm.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", org.pubvm.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
|
||||
bindKeyToMap(im, "PA2", org.pubvm.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
|
||||
bindKeyToMap(im, "PA3", org.pubvm.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
|
||||
|
||||
// Clear
|
||||
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
||||
|
||||
// Create actions for all bound keys
|
||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
||||
am.put("j3270-TAB", createAction(() -> handleTab(false)));
|
||||
am.put("j3270-shift TAB", createAction(() -> handleTab(true)));
|
||||
am.put("j3270-UP", createAction(() -> handleCursor("up")));
|
||||
am.put("j3270-DOWN", createAction(() -> handleCursor("down")));
|
||||
am.put("j3270-LEFT", createAction(() -> handleCursor("left")));
|
||||
am.put("j3270-RIGHT", createAction(() -> handleCursor("right")));
|
||||
am.put("j3270-HOME", createAction(() -> handleCursor("home")));
|
||||
am.put("j3270-PAGE_UP", createAction(() -> handlePF(7)));
|
||||
am.put("j3270-PAGE_DOWN", createAction(() -> handlePF(8)));
|
||||
am.put("j3270-END", createAction(this::handleEraseEOF));
|
||||
am.put("j3270-DELETE", createAction(this::handleDelete));
|
||||
am.put("j3270-BACK_SPACE", createAction(this::handleBackspace));
|
||||
am.put("j3270-INSERT", createAction(this::handleInsert));
|
||||
am.put("j3270-CLEAR", createAction(this::handleClear));
|
||||
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
final int pf = i;
|
||||
am.put("j3270-PF" + i, createAction(() -> handlePF(pf)));
|
||||
}
|
||||
|
||||
am.put("j3270-PA1", createAction(() -> handlePA(1)));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.processKeyEvent(e);
|
||||
}
|
||||
|
||||
private AbstractAction createAction(Runnable r) {
|
||||
return new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
r.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Key action handlers ==========
|
||||
|
||||
private void handleEnter() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendEnter();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReset() {
|
||||
if (client != null) {
|
||||
client.reset();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTab(boolean shift) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (shift)
|
||||
client.backTab();
|
||||
else
|
||||
client.tab();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePF(int n) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendPF(n);
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePA(int n) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendPA(n);
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEraseEOF() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.getInputProcessor().eraseEof();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDelete() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.getInputProcessor().deleteChar();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBackspace() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.getInputProcessor().backspace();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInsert() {
|
||||
if (client != null) {
|
||||
var ip = client.getInputProcessor();
|
||||
ip.setInsertMode(!ip.isInsertMode());
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClear() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendClear();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshScreen() {
|
||||
repaint();
|
||||
// Notify parent to update status bar too
|
||||
Container parent = getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof JFrame) {
|
||||
parent.repaint();
|
||||
break;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFont() {
|
||||
currentFontSize = org.pubvm.j3270.config.Settings.getFontSize();
|
||||
String fontFamily = org.pubvm.j3270.config.Settings.getFontFamily();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void setupColors() {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
hostColors[i] = org.pubvm.j3270.config.Settings.getColorOverride(i, DEFAULT_HOST_COLORS[i]);
|
||||
}
|
||||
monoNormal = org.pubvm.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL);
|
||||
monoIntensify = org.pubvm.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
|
||||
monoProtected = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
|
||||
monoProtectedHigh = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
|
||||
bgColor = org.pubvm.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
|
||||
setBackground(bgColor);
|
||||
}
|
||||
|
||||
public void reloadSettings() {
|
||||
setupColors();
|
||||
setupKeyBindings();
|
||||
setupFont();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setFontSize(int size) {
|
||||
currentFontSize = size;
|
||||
org.pubvm.j3270.config.Settings.setFontSize(size);
|
||||
terminalFont = terminalFont.deriveFont((float) size);
|
||||
updateCellSize();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public int getFontSize() {
|
||||
return currentFontSize;
|
||||
}
|
||||
|
||||
private void updateCellSize() {
|
||||
FontMetrics fm = getFontMetrics(terminalFont);
|
||||
cellWidth = fm.charWidth('M');
|
||||
cellHeight = fm.getHeight();
|
||||
fontAscent = fm.getAscent();
|
||||
fontDescent = fm.getDescent();
|
||||
}
|
||||
|
||||
private void setupCursorBlink() {
|
||||
blinkTimer = new Timer(530, e -> {
|
||||
cursorVisible = !cursorVisible;
|
||||
repaint();
|
||||
});
|
||||
blinkTimer.start();
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
if (client != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
// Always size to the MAXIMUM (alternate) screen dimensions,
|
||||
// matching how x3270 works — the window shows the full model size
|
||||
int displayCols = sb.getMaxCols();
|
||||
int displayRows = sb.getMaxRows();
|
||||
return new Dimension(displayCols * cellWidth + padding * 2,
|
||||
displayRows * cellHeight + padding * 2);
|
||||
}
|
||||
return new Dimension(80 * cellWidth + padding * 2, 24 * cellHeight + padding * 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
Graphics2D g2 = (Graphics2D) g;
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||
|
||||
// Manually clear the entire graphics bounds to ensure no ghosting on
|
||||
// resize/clip
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getRows();
|
||||
int cols = sb.getCols();
|
||||
boolean isColorModel = client.getConfig().getModel().isColor();
|
||||
|
||||
// Track current field attribute for monochrome color decisions
|
||||
byte currentFA = 0;
|
||||
ExtendedAttribute currentFieldEa = null;
|
||||
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int col = 0; col < cols; col++) {
|
||||
int baddr = row * cols + col;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
|
||||
int x = padding + col * cellWidth;
|
||||
int y = padding + row * cellHeight;
|
||||
|
||||
// Determine colors and attributes
|
||||
Color fgColor;
|
||||
Color bgColor;
|
||||
boolean bold = false;
|
||||
boolean underline = false;
|
||||
boolean reverse = false;
|
||||
|
||||
if (ea.isFieldAttribute()) {
|
||||
currentFA = ea.fa;
|
||||
currentFieldEa = ea;
|
||||
// Field attributes display as blanks
|
||||
g2.setColor(this.bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine foreground color
|
||||
if (isColorModel) {
|
||||
fgColor = getColorForAttribute(ea, currentFieldEa, currentFA);
|
||||
bgColor = getBackgroundForAttribute(ea, currentFieldEa);
|
||||
} else {
|
||||
fgColor = getMonoColor(currentFA);
|
||||
bgColor = this.bgColor;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Field attribute implicit intensify
|
||||
if (faIsHigh(currentFA & 0xFF))
|
||||
bold = true;
|
||||
|
||||
// Handle invisible fields (zero intensity)
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
g2.setColor(this.bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply reverse video
|
||||
if (reverse) {
|
||||
Color tmp = fgColor;
|
||||
fgColor = bgColor;
|
||||
bgColor = tmp;
|
||||
}
|
||||
|
||||
// Draw background
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
|
||||
// Draw character
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
||||
}
|
||||
|
||||
// Draw underline
|
||||
if (underline) {
|
||||
g2.setColor(fgColor);
|
||||
g2.drawLine(x, y + cellHeight - fontDescent,
|
||||
x + cellWidth - 1, y + cellHeight - fontDescent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int curRow = curAddr / cols;
|
||||
int curCol = curAddr % cols;
|
||||
int cx = padding + curCol * cellWidth;
|
||||
int cy = padding + curRow * cellHeight;
|
||||
|
||||
g2.setColor(new Color(255, 255, 255, 180));
|
||||
g2.setXORMode(bgColor);
|
||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||
g2.setPaintMode();
|
||||
}
|
||||
}
|
||||
|
||||
private Color getColorForAttribute(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) {
|
||||
return hostColors[fg - 0xf0];
|
||||
}
|
||||
if (faIsProtected(currentFA & 0xFF)) {
|
||||
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_BLUE];
|
||||
}
|
||||
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_RED] : hostColors[HOST_COLOR_GREEN];
|
||||
}
|
||||
|
||||
private Color getBackgroundForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa) {
|
||||
int bg = ea.bg != 0 ? (ea.bg & 0xFF)
|
||||
: (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;
|
||||
}
|
||||
return hostColors[idx];
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
private Color getMonoColor(byte fa) {
|
||||
if (faIsProtected(fa & 0xFF)) {
|
||||
return faIsHigh(fa & 0xFF) ? monoProtectedHigh : monoProtected;
|
||||
}
|
||||
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (blinkTimer != null)
|
||||
blinkTimer.stop();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user