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
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("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,24 +121,315 @@ 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);
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);
}
/**
* Use InputMap/ActionMap (key bindings) instead of KeyListener.
* This works reliably even inside a JScrollPane the WHEN_FOCUSED
@@ -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);
+4 -3
View File
@@ -39,6 +39,7 @@ echo "Executable JAR created at: $BUILD_DIR/j3270.jar"
# ---------------------------------------------------------
# Run the newly packaged JAR
# ---------------------------------------------------------
exec java -Dapple.laf.useScreenMenuBar=true \
-Dapple.awt.application.name=j3270 \
-jar "$BUILD_DIR/j3270.jar" "$@"
#exec java -Dapple.laf.useScreenMenuBar=true \
# -Dapple.awt.application.name=j3270 \
# -jar "$BUILD_DIR/j3270.jar" "$@"
nohup java -jar build/j3270.jar &