All current bugs fixed

This commit is contained in:
2026-04-20 20:32:50 -04:00
parent 66f4b46167
commit 6efa9d5c76
17 changed files with 678 additions and 30 deletions
@@ -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);