All current bugs fixed
This commit is contained in:
@@ -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("Reset Font", KeyEvent.VK_0, () -> {
|
||||
terminalPanel.setFontSize(16);
|
||||
pack();
|
||||
terminalPanel.guardedPack();
|
||||
}));
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
@@ -200,8 +200,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
|
||||
|
||||
// Resize window to match model
|
||||
terminalPanel.revalidate();
|
||||
pack();
|
||||
terminalPanel.guardedPack();
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
@@ -233,9 +232,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
private void adjustFontSize(int delta) {
|
||||
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);
|
||||
pack();
|
||||
// Pack after font change — setFontSize sets the resize guard
|
||||
// to prevent the componentResized from re-triggering autoFitFont
|
||||
terminalPanel.guardedPack();
|
||||
}
|
||||
|
||||
// ========== ConnectionListener ==========
|
||||
@@ -249,8 +250,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
// Auto-resize and focus on first full session
|
||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||
terminalPanel.revalidate();
|
||||
pack();
|
||||
terminalPanel.guardedPack();
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
@@ -293,8 +293,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
terminalPanel.revalidate();
|
||||
pack();
|
||||
terminalPanel.guardedPack();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.pubvm.j3270.config;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.awt.Color;
|
||||
import java.io.*;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -171,6 +172,16 @@ public class Settings {
|
||||
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) {
|
||||
switch (section) {
|
||||
case "appearance":
|
||||
@@ -189,6 +200,7 @@ public class Settings {
|
||||
break;
|
||||
case "autoConnectHost": setAutoConnectHost(value); break;
|
||||
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
default:
|
||||
log.warning("Unknown behavior key: " + key);
|
||||
}
|
||||
@@ -217,4 +229,82 @@ public class Settings {
|
||||
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.Map;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
|
||||
public class SettingsDialog extends JDialog {
|
||||
|
||||
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
|
||||
private JComboBox<String> fontBox;
|
||||
private JSpinner fontSizeSpinner;
|
||||
@@ -25,6 +37,7 @@ public class SettingsDialog extends JDialog {
|
||||
private JPanel autoConnectPanel;
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JCheckBox blockSelectCheck;
|
||||
|
||||
// Advanced tab state tracking
|
||||
private final Color[] tempHostColors = new Color[16];
|
||||
@@ -49,10 +62,15 @@ public class SettingsDialog extends JDialog {
|
||||
tabbedPane.addTab("Advanced", createAdvancedPanel());
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton btnExport = new JButton("Export Config...");
|
||||
JButton btnOk = new JButton("OK");
|
||||
JButton btnApply = new JButton("Apply");
|
||||
JButton btnCancel = new JButton("Cancel");
|
||||
|
||||
btnExport.addActionListener((ActionEvent e) -> {
|
||||
exportConfig();
|
||||
});
|
||||
|
||||
btnOk.addActionListener((ActionEvent e) -> {
|
||||
boolean success = applySettings();
|
||||
if (success) {
|
||||
@@ -68,14 +86,145 @@ public class SettingsDialog extends JDialog {
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(btnExport);
|
||||
buttonPanel.add(Box.createHorizontalStrut(20));
|
||||
buttonPanel.add(btnApply);
|
||||
buttonPanel.add(btnCancel);
|
||||
buttonPanel.add(btnOk);
|
||||
|
||||
getContentPane().add(tabbedPane, BorderLayout.CENTER);
|
||||
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() {
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
@@ -174,8 +323,14 @@ public class SettingsDialog extends JDialog {
|
||||
});
|
||||
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
|
||||
|
||||
// Placeholder for potentially more behavior options below
|
||||
// Block select mode checkbox
|
||||
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;
|
||||
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]);
|
||||
tempHostColors[i] = init;
|
||||
JPanel cb = new JPanel();
|
||||
cb.setName("colorSwatch");
|
||||
cb.setBackground(init);
|
||||
cb.setPreferredSize(new Dimension(30, 30));
|
||||
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
||||
@@ -238,6 +394,7 @@ public class SettingsDialog extends JDialog {
|
||||
JPanel p = new JPanel(new BorderLayout());
|
||||
p.add(new JLabel(mk, SwingConstants.CENTER), BorderLayout.NORTH);
|
||||
JPanel cb = new JPanel();
|
||||
cb.setName("colorSwatch");
|
||||
cb.setBackground(init);
|
||||
cb.setPreferredSize(new Dimension(40, 40));
|
||||
cb.setBorder(new LineBorder(Color.DARK_GRAY));
|
||||
@@ -331,6 +488,8 @@ public class SettingsDialog extends JDialog {
|
||||
captureDialog.setSize(320, 100);
|
||||
captureDialog.setLocationRelativeTo(this);
|
||||
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
|
||||
lbl.setForeground(DARK_FG);
|
||||
captureDialog.getContentPane().setBackground(DARK_BG);
|
||||
captureDialog.add(lbl);
|
||||
captureDialog.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
@@ -442,6 +601,8 @@ public class SettingsDialog extends JDialog {
|
||||
return action; // nav keys default to their own name
|
||||
}
|
||||
|
||||
// ========== Apply Settings ==========
|
||||
|
||||
private boolean applySettings() {
|
||||
try {
|
||||
// Apply Appearance
|
||||
@@ -464,6 +625,9 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
// Block select mode
|
||||
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
|
||||
|
||||
// Propagate visual changes to the app
|
||||
// Save Colors
|
||||
for (int i=0; i<16; i++) {
|
||||
@@ -487,4 +651,43 @@ public class SettingsDialog extends JDialog {
|
||||
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 java.awt.*;
|
||||
import java.awt.datatransfer.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
@@ -44,6 +45,39 @@ public class TerminalPanel extends JPanel {
|
||||
// Background
|
||||
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
|
||||
public static final Color[] DEFAULT_HOST_COLORS = {
|
||||
new Color(0, 0, 0), // 0: Neutral Black
|
||||
@@ -87,22 +121,313 @@ public class TerminalPanel extends JPanel {
|
||||
setupFont();
|
||||
setupCursorBlink();
|
||||
|
||||
// Handle mouse clicks to position cursor and grab focus
|
||||
addMouseListener(new MouseAdapter() {
|
||||
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||
|
||||
// Handle mouse clicks to position cursor, selection, and grab focus
|
||||
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
requestFocusInWindow();
|
||||
|
||||
// Right-click context menu
|
||||
if (SwingUtilities.isRightMouseButton(e)) {
|
||||
showContextMenu(e);
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
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));
|
||||
|
||||
// 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
|
||||
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
|
||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||
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-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++) {
|
||||
final int pf = i;
|
||||
am.put("j3270-PF" + i, createAction(() -> handlePF(pf)));
|
||||
@@ -365,6 +699,7 @@ public class TerminalPanel extends JPanel {
|
||||
setupColors();
|
||||
setupKeyBindings();
|
||||
setupFont();
|
||||
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
@@ -374,6 +709,7 @@ public class TerminalPanel extends JPanel {
|
||||
org.pubvm.j3270.config.Settings.setFontSize(size);
|
||||
terminalFont = terminalFont.deriveFont((float) size);
|
||||
updateCellSize();
|
||||
resizeGuard = true;
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
@@ -406,10 +742,11 @@ public class TerminalPanel extends JPanel {
|
||||
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();
|
||||
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
|
||||
// When the host switches to alternate screen, onScreenSizeChanged fires
|
||||
// and the frame re-packs.
|
||||
int displayCols = sb.getCols();
|
||||
int displayRows = sb.getRows();
|
||||
return new Dimension(displayCols * cellWidth + 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_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||
|
||||
// Manually clear the entire graphics bounds to ensure no ghosting on
|
||||
// resize/clip
|
||||
// Clear entire panel with background color
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||
|
||||
// Compute centered offsets for the grid
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getRows();
|
||||
int cols = sb.getCols();
|
||||
@@ -446,8 +786,8 @@ public class TerminalPanel extends JPanel {
|
||||
int baddr = row * cols + col;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
|
||||
int x = padding + col * cellWidth;
|
||||
int y = padding + row * cellHeight;
|
||||
int x = ox + col * cellWidth;
|
||||
int y = oy + row * cellHeight;
|
||||
|
||||
// Determine colors and attributes
|
||||
Color fgColor;
|
||||
@@ -462,6 +802,11 @@ public class TerminalPanel extends JPanel {
|
||||
// Field attributes display as blanks
|
||||
g2.setColor(this.bgColor);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -493,6 +838,10 @@ public class TerminalPanel extends JPanel {
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
g2.setColor(this.bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -522,6 +871,12 @@ public class TerminalPanel extends JPanel {
|
||||
g2.drawLine(x, 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 curRow = curAddr / cols;
|
||||
int curCol = curAddr % cols;
|
||||
int cx = padding + curCol * cellWidth;
|
||||
int cy = padding + curRow * cellHeight;
|
||||
int cx = ox + curCol * cellWidth;
|
||||
int cy = oy + curRow * cellHeight;
|
||||
|
||||
g2.setColor(new Color(255, 255, 255, 180));
|
||||
g2.setXORMode(bgColor);
|
||||
|
||||
Reference in New Issue
Block a user