All current bugs fixed
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -109,7 +109,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
|
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
|
||||||
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
|
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
|
||||||
terminalPanel.setFontSize(16);
|
terminalPanel.setFontSize(16);
|
||||||
pack();
|
terminalPanel.guardedPack();
|
||||||
}));
|
}));
|
||||||
menuBar.add(viewMenu);
|
menuBar.add(viewMenu);
|
||||||
|
|
||||||
@@ -200,8 +200,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
|
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
|
||||||
|
|
||||||
// Resize window to match model
|
// Resize window to match model
|
||||||
terminalPanel.revalidate();
|
terminalPanel.guardedPack();
|
||||||
pack();
|
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -233,9 +232,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
private void adjustFontSize(int delta) {
|
private void adjustFontSize(int delta) {
|
||||||
int current = terminalPanel.getFontSize();
|
int current = terminalPanel.getFontSize();
|
||||||
int newSize = Math.max(8, Math.min(36, current + delta));
|
int newSize = Math.max(8, Math.min(72, current + delta));
|
||||||
terminalPanel.setFontSize(newSize);
|
terminalPanel.setFontSize(newSize);
|
||||||
pack();
|
// Pack after font change — setFontSize sets the resize guard
|
||||||
|
// to prevent the componentResized from re-triggering autoFitFont
|
||||||
|
terminalPanel.guardedPack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ConnectionListener ==========
|
// ========== ConnectionListener ==========
|
||||||
@@ -249,8 +250,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
// Auto-resize and focus on first full session
|
// Auto-resize and focus on first full session
|
||||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||||
terminalPanel.revalidate();
|
terminalPanel.guardedPack();
|
||||||
pack();
|
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -293,8 +293,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
@Override
|
@Override
|
||||||
public void onScreenSizeChanged(int rows, int cols) {
|
public void onScreenSizeChanged(int rows, int cols) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
terminalPanel.revalidate();
|
terminalPanel.guardedPack();
|
||||||
pack();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.pubvm.j3270.config;
|
|||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.io.PrintWriter;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
@@ -171,6 +172,16 @@ public class Settings {
|
|||||||
log.info("Configuration loaded successfully.");
|
log.info("Configuration loaded successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Block select mode ==========
|
||||||
|
|
||||||
|
public static boolean getBlockSelectMode() {
|
||||||
|
return prefs.getBoolean("blockSelectMode", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setBlockSelectMode(boolean block) {
|
||||||
|
prefs.putBoolean("blockSelectMode", block);
|
||||||
|
}
|
||||||
|
|
||||||
private static void applyConfigEntry(String section, String key, String value) {
|
private static void applyConfigEntry(String section, String key, String value) {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "appearance":
|
case "appearance":
|
||||||
@@ -189,6 +200,7 @@ public class Settings {
|
|||||||
break;
|
break;
|
||||||
case "autoConnectHost": setAutoConnectHost(value); break;
|
case "autoConnectHost": setAutoConnectHost(value); break;
|
||||||
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
|
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
|
||||||
|
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||||
default:
|
default:
|
||||||
log.warning("Unknown behavior key: " + key);
|
log.warning("Unknown behavior key: " + key);
|
||||||
}
|
}
|
||||||
@@ -217,4 +229,82 @@ public class Settings {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Export to INI ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export the current configuration to an INI-style file.
|
||||||
|
* Writes all settings including appearance, behavior, colors, and keybindings.
|
||||||
|
*/
|
||||||
|
public static void exportToIniFile(String path) throws IOException {
|
||||||
|
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(path)))) {
|
||||||
|
w.println("; j3270 Configuration File");
|
||||||
|
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
|
||||||
|
w.println("; Load with: java -jar j3270.jar -c " + new File(path).getName());
|
||||||
|
w.println();
|
||||||
|
|
||||||
|
// [appearance]
|
||||||
|
w.println("[appearance]");
|
||||||
|
w.println("fontFamily = " + getFontFamily());
|
||||||
|
w.println("fontSize = " + getFontSize());
|
||||||
|
w.println();
|
||||||
|
|
||||||
|
// [behavior]
|
||||||
|
w.println("[behavior]");
|
||||||
|
w.println("startupBehavior = " + getStartupBehavior().name());
|
||||||
|
String acHost = getAutoConnectHost();
|
||||||
|
if (acHost != null && !acHost.isEmpty()) {
|
||||||
|
w.println("autoConnectHost = " + acHost);
|
||||||
|
w.println("autoConnectPort = " + getAutoConnectPort());
|
||||||
|
}
|
||||||
|
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||||
|
w.println();
|
||||||
|
|
||||||
|
// [colors]
|
||||||
|
w.println("[colors]");
|
||||||
|
// Host colors 0-15
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
String hex = prefs.get("color_" + i, null);
|
||||||
|
if (hex != null) {
|
||||||
|
w.println("color_" + i + " = " + hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mono colors
|
||||||
|
String[] monoKeys = {"NORMAL", "INTENSIFY", "PROTECTED", "PROTECTED_HIGH", "BACKGROUND"};
|
||||||
|
for (String mk : monoKeys) {
|
||||||
|
String hex = prefs.get("mono_" + mk, null);
|
||||||
|
if (hex != null) {
|
||||||
|
w.println("mono_" + mk + " = " + hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.println();
|
||||||
|
|
||||||
|
// [keybindings]
|
||||||
|
w.println("[keybindings]");
|
||||||
|
// Navigation keys
|
||||||
|
String[] navActions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||||
|
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
|
||||||
|
for (String act : navActions) {
|
||||||
|
String val = prefs.get("key_" + act, null);
|
||||||
|
if (val != null) {
|
||||||
|
w.println(act + " = " + val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// PF keys
|
||||||
|
for (int i = 1; i <= 24; i++) {
|
||||||
|
String val = prefs.get("key_PF" + i, null);
|
||||||
|
if (val != null) {
|
||||||
|
w.println("PF" + i + " = " + val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// PA keys
|
||||||
|
for (int i = 1; i <= 3; i++) {
|
||||||
|
String val = prefs.get("key_PA" + i, null);
|
||||||
|
if (val != null) {
|
||||||
|
w.println("PA" + i + " = " + val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("Configuration exported to: " + path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,24 @@ import java.awt.event.*;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.swing.table.DefaultTableModel;
|
import javax.swing.table.DefaultTableModel;
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer;
|
||||||
|
import javax.swing.table.JTableHeader;
|
||||||
import javax.swing.border.LineBorder;
|
import javax.swing.border.LineBorder;
|
||||||
|
import javax.swing.border.TitledBorder;
|
||||||
|
|
||||||
public class SettingsDialog extends JDialog {
|
public class SettingsDialog extends JDialog {
|
||||||
|
|
||||||
private final J3270App parentApp;
|
private final J3270App parentApp;
|
||||||
|
|
||||||
|
// Dark theme colors
|
||||||
|
private static final Color DARK_BG = new Color(43, 43, 43);
|
||||||
|
private static final Color DARK_BG_LIGHTER = new Color(55, 55, 55);
|
||||||
|
private static final Color DARK_FG = new Color(224, 224, 224);
|
||||||
|
private static final Color DARK_BORDER = new Color(70, 70, 70);
|
||||||
|
private static final Color DARK_SELECTION = new Color(75, 110, 175);
|
||||||
|
private static final Color DARK_BUTTON_BG = new Color(60, 63, 65);
|
||||||
|
private static final Color DARK_FIELD_BG = new Color(50, 50, 50);
|
||||||
|
|
||||||
// Appearance tab
|
// Appearance tab
|
||||||
private JComboBox<String> fontBox;
|
private JComboBox<String> fontBox;
|
||||||
private JSpinner fontSizeSpinner;
|
private JSpinner fontSizeSpinner;
|
||||||
@@ -25,6 +37,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
private JPanel autoConnectPanel;
|
private JPanel autoConnectPanel;
|
||||||
private JTextField hostField;
|
private JTextField hostField;
|
||||||
private JTextField portField;
|
private JTextField portField;
|
||||||
|
private JCheckBox blockSelectCheck;
|
||||||
|
|
||||||
// Advanced tab state tracking
|
// Advanced tab state tracking
|
||||||
private final Color[] tempHostColors = new Color[16];
|
private final Color[] tempHostColors = new Color[16];
|
||||||
@@ -49,10 +62,15 @@ public class SettingsDialog extends JDialog {
|
|||||||
tabbedPane.addTab("Advanced", createAdvancedPanel());
|
tabbedPane.addTab("Advanced", createAdvancedPanel());
|
||||||
|
|
||||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
JButton btnExport = new JButton("Export Config...");
|
||||||
JButton btnOk = new JButton("OK");
|
JButton btnOk = new JButton("OK");
|
||||||
JButton btnApply = new JButton("Apply");
|
JButton btnApply = new JButton("Apply");
|
||||||
JButton btnCancel = new JButton("Cancel");
|
JButton btnCancel = new JButton("Cancel");
|
||||||
|
|
||||||
|
btnExport.addActionListener((ActionEvent e) -> {
|
||||||
|
exportConfig();
|
||||||
|
});
|
||||||
|
|
||||||
btnOk.addActionListener((ActionEvent e) -> {
|
btnOk.addActionListener((ActionEvent e) -> {
|
||||||
boolean success = applySettings();
|
boolean success = applySettings();
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -68,14 +86,145 @@ public class SettingsDialog extends JDialog {
|
|||||||
dispose();
|
dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
buttonPanel.add(btnExport);
|
||||||
|
buttonPanel.add(Box.createHorizontalStrut(20));
|
||||||
buttonPanel.add(btnApply);
|
buttonPanel.add(btnApply);
|
||||||
buttonPanel.add(btnCancel);
|
buttonPanel.add(btnCancel);
|
||||||
buttonPanel.add(btnOk);
|
buttonPanel.add(btnOk);
|
||||||
|
|
||||||
getContentPane().add(tabbedPane, BorderLayout.CENTER);
|
getContentPane().add(tabbedPane, BorderLayout.CENTER);
|
||||||
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
|
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
// Apply dark theme to all components for cross-platform readability
|
||||||
|
applyDarkTheme(getContentPane());
|
||||||
|
applyDarkTheme(tabbedPane);
|
||||||
|
applyDarkTheme(buttonPanel);
|
||||||
|
getContentPane().setBackground(DARK_BG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Dark Theme Utility ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively apply dark theme to a component and all its children.
|
||||||
|
* Ensures the Settings dialog is readable on Windows, Linux, and macOS.
|
||||||
|
*/
|
||||||
|
private void applyDarkTheme(Component comp) {
|
||||||
|
if (comp instanceof JTabbedPane) {
|
||||||
|
JTabbedPane tp = (JTabbedPane) comp;
|
||||||
|
tp.setBackground(DARK_BG);
|
||||||
|
tp.setForeground(DARK_FG);
|
||||||
|
for (int i = 0; i < tp.getTabCount(); i++) {
|
||||||
|
applyDarkTheme(tp.getComponentAt(i));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JTable) {
|
||||||
|
JTable table = (JTable) comp;
|
||||||
|
table.setBackground(DARK_FIELD_BG);
|
||||||
|
table.setForeground(DARK_FG);
|
||||||
|
table.setSelectionBackground(DARK_SELECTION);
|
||||||
|
table.setSelectionForeground(Color.WHITE);
|
||||||
|
table.setGridColor(DARK_BORDER);
|
||||||
|
JTableHeader header = table.getTableHeader();
|
||||||
|
if (header != null) {
|
||||||
|
header.setBackground(DARK_BG_LIGHTER);
|
||||||
|
header.setForeground(DARK_FG);
|
||||||
|
DefaultTableCellRenderer headerRenderer = new DefaultTableCellRenderer();
|
||||||
|
headerRenderer.setBackground(DARK_BG_LIGHTER);
|
||||||
|
headerRenderer.setForeground(DARK_FG);
|
||||||
|
header.setDefaultRenderer(headerRenderer);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JScrollPane) {
|
||||||
|
JScrollPane sp = (JScrollPane) comp;
|
||||||
|
sp.setBackground(DARK_BG);
|
||||||
|
sp.getViewport().setBackground(DARK_FIELD_BG);
|
||||||
|
applyDarkTheme(sp.getViewport().getView());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JButton) {
|
||||||
|
JButton btn = (JButton) comp;
|
||||||
|
btn.setBackground(DARK_BUTTON_BG);
|
||||||
|
btn.setForeground(DARK_FG);
|
||||||
|
btn.setFocusPainted(false);
|
||||||
|
btn.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
new LineBorder(DARK_BORDER, 1),
|
||||||
|
BorderFactory.createEmptyBorder(3, 10, 3, 10)));
|
||||||
|
btn.setOpaque(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JComboBox) {
|
||||||
|
JComboBox<?> cb = (JComboBox<?>) comp;
|
||||||
|
cb.setBackground(DARK_FIELD_BG);
|
||||||
|
cb.setForeground(DARK_FG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JSpinner) {
|
||||||
|
JSpinner sp = (JSpinner) comp;
|
||||||
|
sp.setBackground(DARK_FIELD_BG);
|
||||||
|
sp.setForeground(DARK_FG);
|
||||||
|
JComponent editor = sp.getEditor();
|
||||||
|
if (editor instanceof JSpinner.DefaultEditor) {
|
||||||
|
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
|
||||||
|
tf.setBackground(DARK_FIELD_BG);
|
||||||
|
tf.setForeground(DARK_FG);
|
||||||
|
tf.setCaretColor(DARK_FG);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JTextField) {
|
||||||
|
JTextField tf = (JTextField) comp;
|
||||||
|
tf.setBackground(DARK_FIELD_BG);
|
||||||
|
tf.setForeground(DARK_FG);
|
||||||
|
tf.setCaretColor(DARK_FG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JCheckBox) {
|
||||||
|
JCheckBox cb = (JCheckBox) comp;
|
||||||
|
cb.setBackground(DARK_BG);
|
||||||
|
cb.setForeground(DARK_FG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof JLabel) {
|
||||||
|
comp.setForeground(DARK_FG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic panel / container
|
||||||
|
// Skip color swatch panels — their background IS the color
|
||||||
|
if (comp instanceof JPanel && "colorSwatch".equals(comp.getName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
comp.setBackground(DARK_BG);
|
||||||
|
comp.setForeground(DARK_FG);
|
||||||
|
|
||||||
|
if (comp instanceof JPanel) {
|
||||||
|
JPanel panel = (JPanel) comp;
|
||||||
|
// Style titled borders
|
||||||
|
if (panel.getBorder() instanceof TitledBorder) {
|
||||||
|
TitledBorder tb = (TitledBorder) panel.getBorder();
|
||||||
|
tb.setTitleColor(DARK_FG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp instanceof Container) {
|
||||||
|
for (Component child : ((Container) comp).getComponents()) {
|
||||||
|
applyDarkTheme(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Tab Panels ==========
|
||||||
|
|
||||||
private JPanel createAppearancePanel() {
|
private JPanel createAppearancePanel() {
|
||||||
JPanel panel = new JPanel(new GridBagLayout());
|
JPanel panel = new JPanel(new GridBagLayout());
|
||||||
GridBagConstraints gbc = new GridBagConstraints();
|
GridBagConstraints gbc = new GridBagConstraints();
|
||||||
@@ -174,8 +323,14 @@ public class SettingsDialog extends JDialog {
|
|||||||
});
|
});
|
||||||
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
|
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
|
||||||
|
|
||||||
// Placeholder for potentially more behavior options below
|
// Block select mode checkbox
|
||||||
gbc.gridy = 2;
|
gbc.gridy = 2;
|
||||||
|
gbc.gridwidth = 2;
|
||||||
|
blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
|
||||||
|
panel.add(blockSelectCheck, gbc);
|
||||||
|
|
||||||
|
// Placeholder for potentially more behavior options below
|
||||||
|
gbc.gridy = 3;
|
||||||
gbc.weighty = 1.0;
|
gbc.weighty = 1.0;
|
||||||
panel.add(Box.createGlue(), gbc);
|
panel.add(Box.createGlue(), gbc);
|
||||||
|
|
||||||
@@ -204,6 +359,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
|
Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
|
||||||
tempHostColors[i] = init;
|
tempHostColors[i] = init;
|
||||||
JPanel cb = new JPanel();
|
JPanel cb = new JPanel();
|
||||||
|
cb.setName("colorSwatch");
|
||||||
cb.setBackground(init);
|
cb.setBackground(init);
|
||||||
cb.setPreferredSize(new Dimension(30, 30));
|
cb.setPreferredSize(new Dimension(30, 30));
|
||||||
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
||||||
@@ -238,6 +394,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
JPanel p = new JPanel(new BorderLayout());
|
JPanel p = new JPanel(new BorderLayout());
|
||||||
p.add(new JLabel(mk, SwingConstants.CENTER), BorderLayout.NORTH);
|
p.add(new JLabel(mk, SwingConstants.CENTER), BorderLayout.NORTH);
|
||||||
JPanel cb = new JPanel();
|
JPanel cb = new JPanel();
|
||||||
|
cb.setName("colorSwatch");
|
||||||
cb.setBackground(init);
|
cb.setBackground(init);
|
||||||
cb.setPreferredSize(new Dimension(40, 40));
|
cb.setPreferredSize(new Dimension(40, 40));
|
||||||
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
||||||
@@ -331,6 +488,8 @@ public class SettingsDialog extends JDialog {
|
|||||||
captureDialog.setSize(320, 100);
|
captureDialog.setSize(320, 100);
|
||||||
captureDialog.setLocationRelativeTo(this);
|
captureDialog.setLocationRelativeTo(this);
|
||||||
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
|
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
|
||||||
|
lbl.setForeground(DARK_FG);
|
||||||
|
captureDialog.getContentPane().setBackground(DARK_BG);
|
||||||
captureDialog.add(lbl);
|
captureDialog.add(lbl);
|
||||||
captureDialog.addKeyListener(new KeyAdapter() {
|
captureDialog.addKeyListener(new KeyAdapter() {
|
||||||
@Override
|
@Override
|
||||||
@@ -442,6 +601,8 @@ public class SettingsDialog extends JDialog {
|
|||||||
return action; // nav keys default to their own name
|
return action; // nav keys default to their own name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Apply Settings ==========
|
||||||
|
|
||||||
private boolean applySettings() {
|
private boolean applySettings() {
|
||||||
try {
|
try {
|
||||||
// Apply Appearance
|
// Apply Appearance
|
||||||
@@ -464,6 +625,9 @@ public class SettingsDialog extends JDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block select mode
|
||||||
|
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
|
||||||
|
|
||||||
// Propagate visual changes to the app
|
// Propagate visual changes to the app
|
||||||
// Save Colors
|
// Save Colors
|
||||||
for (int i=0; i<16; i++) {
|
for (int i=0; i<16; i++) {
|
||||||
@@ -487,4 +651,43 @@ public class SettingsDialog extends JDialog {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Export Config ==========
|
||||||
|
|
||||||
|
private void exportConfig() {
|
||||||
|
JFileChooser fc = new JFileChooser();
|
||||||
|
fc.setDialogTitle("Export Configuration");
|
||||||
|
fc.setSelectedFile(new java.io.File("j3270.ini"));
|
||||||
|
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini"));
|
||||||
|
|
||||||
|
int result = fc.showSaveDialog(this);
|
||||||
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
|
java.io.File file = fc.getSelectedFile();
|
||||||
|
// Ensure .ini extension
|
||||||
|
if (!file.getName().toLowerCase().endsWith(".ini")) {
|
||||||
|
file = new java.io.File(file.getAbsolutePath() + ".ini");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm overwrite
|
||||||
|
if (file.exists()) {
|
||||||
|
int confirm = JOptionPane.showConfirmDialog(this,
|
||||||
|
"File already exists. Overwrite?", "Confirm Overwrite",
|
||||||
|
JOptionPane.YES_NO_OPTION);
|
||||||
|
if (confirm != JOptionPane.YES_OPTION) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Apply current dialog state first so the export reflects latest changes
|
||||||
|
applySettings();
|
||||||
|
Settings.exportToIniFile(file.getAbsolutePath());
|
||||||
|
JOptionPane.showMessageDialog(this,
|
||||||
|
"Configuration exported to:\n" + file.getAbsolutePath(),
|
||||||
|
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(this,
|
||||||
|
"Export failed: " + ex.getMessage(),
|
||||||
|
"Export Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.lib3270j.screen.ScreenBuffer;
|
|||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.datatransfer.*;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
|
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||||
@@ -44,6 +45,39 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Background
|
// Background
|
||||||
private Color bgColor;
|
private Color bgColor;
|
||||||
|
|
||||||
|
// ========== Selection / Copy-Paste state ==========
|
||||||
|
private boolean blockSelectMode = false;
|
||||||
|
private int selectionStartRow = -1, selectionStartCol = -1;
|
||||||
|
private int selectionEndRow = -1, selectionEndCol = -1;
|
||||||
|
private boolean isDragging = false;
|
||||||
|
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
||||||
|
|
||||||
|
// ========== Resize guard ==========
|
||||||
|
private boolean resizeGuard = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
if (client != null) termCols = client.getScreenBuffer().getCols();
|
||||||
|
int gridW = termCols * cellWidth;
|
||||||
|
int extra = getWidth() - gridW;
|
||||||
|
return Math.max(padding, extra / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the vertical render offset to center the grid within the panel.
|
||||||
|
*/
|
||||||
|
private int getRenderOffsetY() {
|
||||||
|
int termRows = 24;
|
||||||
|
if (client != null) termRows = client.getScreenBuffer().getRows();
|
||||||
|
int gridH = termRows * cellHeight;
|
||||||
|
int extra = getHeight() - gridH;
|
||||||
|
return Math.max(padding, extra / 2);
|
||||||
|
}
|
||||||
|
|
||||||
// Default Host color mapping
|
// Default Host color mapping
|
||||||
public static final Color[] DEFAULT_HOST_COLORS = {
|
public static final Color[] DEFAULT_HOST_COLORS = {
|
||||||
new Color(0, 0, 0), // 0: Neutral Black
|
new Color(0, 0, 0), // 0: Neutral Black
|
||||||
@@ -87,22 +121,313 @@ public class TerminalPanel extends JPanel {
|
|||||||
setupFont();
|
setupFont();
|
||||||
setupCursorBlink();
|
setupCursorBlink();
|
||||||
|
|
||||||
// Handle mouse clicks to position cursor and grab focus
|
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||||
addMouseListener(new MouseAdapter() {
|
|
||||||
|
// Handle mouse clicks to position cursor, selection, and grab focus
|
||||||
|
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void mousePressed(MouseEvent e) {
|
public void mousePressed(MouseEvent e) {
|
||||||
requestFocusInWindow();
|
requestFocusInWindow();
|
||||||
|
|
||||||
|
// Right-click context menu
|
||||||
|
if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
|
showContextMenu(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
int col = (e.getX() - padding) / cellWidth;
|
|
||||||
int row = (e.getY() - padding) / cellHeight;
|
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
if (row >= 0 && row < sb.getRows() && col >= 0 && col < sb.getCols()) {
|
int ox = getRenderOffsetX();
|
||||||
sb.setCursorAddress(row * sb.getCols() + col);
|
int oy = getRenderOffsetY();
|
||||||
repaint();
|
int col = (e.getX() - ox) / cellWidth;
|
||||||
|
int row = (e.getY() - oy) / cellHeight;
|
||||||
|
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
||||||
|
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
||||||
|
|
||||||
|
// Start selection
|
||||||
|
selectionStartRow = row;
|
||||||
|
selectionStartCol = col;
|
||||||
|
selectionEndRow = row;
|
||||||
|
selectionEndCol = col;
|
||||||
|
isDragging = true;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseDragged(MouseEvent e) {
|
||||||
|
if (isDragging && client != null && client.getConnectionState().isFullSession()) {
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
int ox = getRenderOffsetX();
|
||||||
|
int oy = getRenderOffsetY();
|
||||||
|
int col = (e.getX() - ox) / cellWidth;
|
||||||
|
int row = (e.getY() - oy) / cellHeight;
|
||||||
|
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
||||||
|
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
||||||
|
|
||||||
|
selectionEndRow = row;
|
||||||
|
selectionEndCol = col;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
// If start == end, treat as a click (position cursor, clear selection)
|
||||||
|
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
||||||
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
||||||
|
clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addMouseListener(mouseHandler);
|
||||||
|
addMouseMotionListener(mouseHandler);
|
||||||
|
|
||||||
|
// Handle resize: auto-fit font to window size
|
||||||
|
addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentResized(ComponentEvent e) {
|
||||||
|
if (resizeGuard) return;
|
||||||
|
autoFitFont();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Selection / Copy-Paste ==========
|
||||||
|
|
||||||
|
private void clearSelection() {
|
||||||
|
selectionStartRow = -1;
|
||||||
|
selectionStartCol = -1;
|
||||||
|
selectionEndRow = -1;
|
||||||
|
selectionEndCol = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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();
|
||||||
|
int cols = sb.getCols();
|
||||||
|
|
||||||
|
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||||
|
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||||
|
int c1, c2;
|
||||||
|
|
||||||
|
if (blockSelectMode) {
|
||||||
|
// Block mode: rectangular selection
|
||||||
|
c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||||
|
c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (int r = r1; r <= r2; r++) {
|
||||||
|
if (r > r1) result.append('\n');
|
||||||
|
for (int c = c1; c <= c2; c++) {
|
||||||
|
int baddr = r * cols + c;
|
||||||
|
ExtendedAttribute ea = sb.getCell(baddr);
|
||||||
|
char ch = ea.ucs4;
|
||||||
|
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||||
|
result.append(' ');
|
||||||
|
} else {
|
||||||
|
result.append(ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result.toString();
|
||||||
|
} else {
|
||||||
|
// Line/stream mode: flow from start to end
|
||||||
|
int startAddr, endAddr;
|
||||||
|
if (selectionStartRow < selectionEndRow ||
|
||||||
|
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||||
|
startAddr = selectionStartRow * cols + selectionStartCol;
|
||||||
|
endAddr = selectionEndRow * cols + selectionEndCol;
|
||||||
|
} else {
|
||||||
|
startAddr = selectionEndRow * cols + selectionEndCol;
|
||||||
|
endAddr = selectionStartRow * cols + selectionStartCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
int prevRow = startAddr / cols;
|
||||||
|
for (int addr = startAddr; addr <= endAddr; addr++) {
|
||||||
|
int curRow = addr / cols;
|
||||||
|
if (curRow != prevRow) {
|
||||||
|
result.append('\n');
|
||||||
|
prevRow = curRow;
|
||||||
|
}
|
||||||
|
ExtendedAttribute ea = sb.getCell(addr);
|
||||||
|
char ch = ea.ucs4;
|
||||||
|
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||||
|
result.append(' ');
|
||||||
|
} else {
|
||||||
|
result.append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copySelection() {
|
||||||
|
String text = getSelectedText();
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
StringSelection ss = new StringSelection(text);
|
||||||
|
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pasteClipboard() {
|
||||||
|
if (client == null || !client.getConnectionState().isFullSession()) return;
|
||||||
|
try {
|
||||||
|
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
.getData(DataFlavor.stringFlavor);
|
||||||
|
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) {
|
||||||
|
client.typeCharacter(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshScreen();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Clipboard not available or wrong type — silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showContextMenu(MouseEvent e) {
|
||||||
|
JPopupMenu popup = new JPopupMenu();
|
||||||
|
|
||||||
|
JMenuItem copyItem = new JMenuItem("Copy");
|
||||||
|
copyItem.setEnabled(hasSelection());
|
||||||
|
copyItem.addActionListener(ev -> copySelection());
|
||||||
|
popup.add(copyItem);
|
||||||
|
|
||||||
|
JMenuItem pasteItem = new JMenuItem("Paste");
|
||||||
|
pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession());
|
||||||
|
pasteItem.addActionListener(ev -> pasteClipboard());
|
||||||
|
popup.add(pasteItem);
|
||||||
|
|
||||||
|
popup.addSeparator();
|
||||||
|
|
||||||
|
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||||
|
blockModeItem.addActionListener(ev -> {
|
||||||
|
blockSelectMode = blockModeItem.isSelected();
|
||||||
|
org.pubvm.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
|
||||||
});
|
});
|
||||||
|
popup.add(blockModeItem);
|
||||||
|
|
||||||
|
popup.show(this, e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCellSelected(int row, int col) {
|
||||||
|
if (!hasSelection()) return false;
|
||||||
|
|
||||||
|
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||||
|
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||||
|
|
||||||
|
if (blockSelectMode) {
|
||||||
|
int c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||||
|
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;
|
||||||
|
int startAddr, endAddr;
|
||||||
|
if (selectionStartRow < selectionEndRow ||
|
||||||
|
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||||
|
startAddr = selectionStartRow * cols + selectionStartCol;
|
||||||
|
endAddr = selectionEndRow * cols + selectionEndCol;
|
||||||
|
} else {
|
||||||
|
startAddr = selectionEndRow * cols + selectionEndCol;
|
||||||
|
endAddr = selectionStartRow * cols + selectionStartCol;
|
||||||
|
}
|
||||||
|
return addr >= startAddr && addr <= endAddr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 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();
|
||||||
|
if (panelW <= 0 || panelH <= 0) return;
|
||||||
|
|
||||||
|
int termCols = 80;
|
||||||
|
int termRows = 24;
|
||||||
|
if (client != null) {
|
||||||
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
|
termCols = sb.getCols();
|
||||||
|
termRows = sb.getRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
FontMetrics fm = getFontMetrics(testFont);
|
||||||
|
int testCellW = fm.charWidth('M');
|
||||||
|
int testCellH = fm.getHeight();
|
||||||
|
|
||||||
|
if (testCellW * termCols <= availW && testCellH * termRows <= availH) {
|
||||||
|
bestSize = testSize;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestSize != currentFontSize) {
|
||||||
|
currentFontSize = bestSize;
|
||||||
|
org.pubvm.j3270.config.Settings.setFontSize(bestSize);
|
||||||
|
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();
|
||||||
|
Container top = getTopLevelAncestor();
|
||||||
|
if (top instanceof java.awt.Window) {
|
||||||
|
((java.awt.Window) top).pack();
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,6 +492,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Clear
|
// Clear
|
||||||
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
||||||
|
|
||||||
|
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
|
||||||
|
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");
|
||||||
|
|
||||||
// Create actions for all bound keys
|
// Create actions for all bound keys
|
||||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||||
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
||||||
@@ -185,6 +515,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
am.put("j3270-INSERT", createAction(this::handleInsert));
|
am.put("j3270-INSERT", createAction(this::handleInsert));
|
||||||
am.put("j3270-CLEAR", createAction(this::handleClear));
|
am.put("j3270-CLEAR", createAction(this::handleClear));
|
||||||
|
|
||||||
|
// Copy/Paste actions
|
||||||
|
am.put("j3270-COPY", createAction(this::copySelection));
|
||||||
|
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||||
|
|
||||||
for (int i = 1; i <= 24; i++) {
|
for (int i = 1; i <= 24; i++) {
|
||||||
final int pf = i;
|
final int pf = i;
|
||||||
am.put("j3270-PF" + i, createAction(() -> handlePF(pf)));
|
am.put("j3270-PF" + i, createAction(() -> handlePF(pf)));
|
||||||
@@ -365,6 +699,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
setupColors();
|
setupColors();
|
||||||
setupKeyBindings();
|
setupKeyBindings();
|
||||||
setupFont();
|
setupFont();
|
||||||
|
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||||
revalidate();
|
revalidate();
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
@@ -374,6 +709,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
org.pubvm.j3270.config.Settings.setFontSize(size);
|
org.pubvm.j3270.config.Settings.setFontSize(size);
|
||||||
terminalFont = terminalFont.deriveFont((float) size);
|
terminalFont = terminalFont.deriveFont((float) size);
|
||||||
updateCellSize();
|
updateCellSize();
|
||||||
|
resizeGuard = true;
|
||||||
revalidate();
|
revalidate();
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
@@ -406,10 +742,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
public Dimension getPreferredSize() {
|
public Dimension getPreferredSize() {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
// Always size to the MAXIMUM (alternate) screen dimensions,
|
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
|
||||||
// matching how x3270 works — the window shows the full model size
|
// When the host switches to alternate screen, onScreenSizeChanged fires
|
||||||
int displayCols = sb.getMaxCols();
|
// and the frame re-packs.
|
||||||
int displayRows = sb.getMaxRows();
|
int displayCols = sb.getCols();
|
||||||
|
int displayRows = sb.getRows();
|
||||||
return new Dimension(displayCols * cellWidth + padding * 2,
|
return new Dimension(displayCols * cellWidth + padding * 2,
|
||||||
displayRows * cellHeight + padding * 2);
|
displayRows * cellHeight + padding * 2);
|
||||||
}
|
}
|
||||||
@@ -427,11 +764,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||||
|
|
||||||
// Manually clear the entire graphics bounds to ensure no ghosting on
|
// Clear entire panel with background color
|
||||||
// resize/clip
|
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||||
|
|
||||||
|
// Compute centered offsets for the grid
|
||||||
|
int ox = getRenderOffsetX();
|
||||||
|
int oy = getRenderOffsetY();
|
||||||
|
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
int rows = sb.getRows();
|
int rows = sb.getRows();
|
||||||
int cols = sb.getCols();
|
int cols = sb.getCols();
|
||||||
@@ -446,8 +786,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
int baddr = row * cols + col;
|
int baddr = row * cols + col;
|
||||||
ExtendedAttribute ea = sb.getCell(baddr);
|
ExtendedAttribute ea = sb.getCell(baddr);
|
||||||
|
|
||||||
int x = padding + col * cellWidth;
|
int x = ox + col * cellWidth;
|
||||||
int y = padding + row * cellHeight;
|
int y = oy + row * cellHeight;
|
||||||
|
|
||||||
// Determine colors and attributes
|
// Determine colors and attributes
|
||||||
Color fgColor;
|
Color fgColor;
|
||||||
@@ -462,6 +802,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Field attributes display as blanks
|
// Field attributes display as blanks
|
||||||
g2.setColor(this.bgColor);
|
g2.setColor(this.bgColor);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
// Selection highlight on field attribute cells too
|
||||||
|
if (isCellSelected(row, col)) {
|
||||||
|
g2.setColor(SELECTION_COLOR);
|
||||||
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +838,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (faIsZero(currentFA & 0xFF)) {
|
if (faIsZero(currentFA & 0xFF)) {
|
||||||
g2.setColor(this.bgColor);
|
g2.setColor(this.bgColor);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
if (isCellSelected(row, col)) {
|
||||||
|
g2.setColor(SELECTION_COLOR);
|
||||||
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,6 +871,12 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.drawLine(x, y + cellHeight - fontDescent,
|
g2.drawLine(x, y + cellHeight - fontDescent,
|
||||||
x + cellWidth - 1, y + cellHeight - fontDescent);
|
x + cellWidth - 1, y + cellHeight - fontDescent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw selection highlight
|
||||||
|
if (isCellSelected(row, col)) {
|
||||||
|
g2.setColor(SELECTION_COLOR);
|
||||||
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,8 +885,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
int curAddr = sb.getCursorAddress();
|
int curAddr = sb.getCursorAddress();
|
||||||
int curRow = curAddr / cols;
|
int curRow = curAddr / cols;
|
||||||
int curCol = curAddr % cols;
|
int curCol = curAddr % cols;
|
||||||
int cx = padding + curCol * cellWidth;
|
int cx = ox + curCol * cellWidth;
|
||||||
int cy = padding + curRow * cellHeight;
|
int cy = oy + curRow * cellHeight;
|
||||||
|
|
||||||
g2.setColor(new Color(255, 255, 255, 180));
|
g2.setColor(new Color(255, 255, 255, 180));
|
||||||
g2.setXORMode(bgColor);
|
g2.setXORMode(bgColor);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ echo "Executable JAR created at: $BUILD_DIR/j3270.jar"
|
|||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Run the newly packaged JAR
|
# Run the newly packaged JAR
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
exec java -Dapple.laf.useScreenMenuBar=true \
|
#exec java -Dapple.laf.useScreenMenuBar=true \
|
||||||
-Dapple.awt.application.name=j3270 \
|
# -Dapple.awt.application.name=j3270 \
|
||||||
-jar "$BUILD_DIR/j3270.jar" "$@"
|
# -jar "$BUILD_DIR/j3270.jar" "$@"
|
||||||
|
nohup java -jar build/j3270.jar &
|
||||||
|
|||||||
Reference in New Issue
Block a user