focus fixes

This commit is contained in:
Radek Davidek 2026-01-16 17:40:56 +01:00
parent d116ed7d0f
commit f88a6c10a1
3 changed files with 154 additions and 18 deletions

View File

@ -155,6 +155,42 @@ public class FilePanel extends JPanel {
}); });
add(tabbedPane, BorderLayout.CENTER); add(tabbedPane, BorderLayout.CENTER);
// Click on panel background or empty areas should focus the table
addMouseListenerToComponents(this);
}
/**
* Recursively adds a mouse listener to all components (except buttons/combos)
* to request focus for the active table when clicked.
*/
private void addMouseListenerToComponents(Component comp) {
if (comp == null) return;
// Components that should NOT steal focus back to the table when clicked
boolean isInteractive = comp instanceof JButton ||
comp instanceof JComboBox ||
comp instanceof JTextField ||
comp instanceof JTable;
if (!isInteractive) {
comp.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mousePressed(java.awt.event.MouseEvent e) {
FilePanelTab tab = getCurrentTab();
if (tab != null) {
tab.getFileTable().requestFocusInWindow();
tab.selectLastItem();
}
}
});
}
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
addMouseListenerToComponents(child);
}
}
} }
/** /**
@ -196,6 +232,9 @@ public class FilePanel extends JPanel {
tabbedPane.addTab(tabTitle, tab); tabbedPane.addTab(tabTitle, tab);
tabbedPane.setSelectedComponent(tab); tabbedPane.setSelectedComponent(tab);
// Ensure clicking on empty areas of the tab/scrollpane focuses the table
addMouseListenerToComponents(tab);
// Update path field // Update path field
updatePathField(); updatePathField();
updateTabStyles(); updateTabStyles();
@ -225,6 +264,9 @@ public class FilePanel extends JPanel {
tabbedPane.addTab(tabTitle, tab); tabbedPane.addTab(tabTitle, tab);
tabbedPane.setSelectedComponent(tab); tabbedPane.setSelectedComponent(tab);
// Ensure clicking on empty areas of the tab/scrollpane focuses the table
addMouseListenerToComponents(tab);
updatePathField(); updatePathField();
updateTabStyles(); updateTabStyles();

View File

@ -43,6 +43,9 @@ public class FilePanelTab extends JPanel {
private int sortColumn = -1; // 0=name,1=size,2=date private int sortColumn = -1; // 0=name,1=size,2=date
private boolean sortAscending = true; private boolean sortAscending = true;
private com.kfmanager.config.AppConfig persistedConfig; private com.kfmanager.config.AppConfig persistedConfig;
// Track last selection to restore it if focus is requested on empty area
private int lastValidRow = 0;
private int lastValidBriefColumn = 0;
// If an archive was opened, we may extract it to a temp directory; track it so // If an archive was opened, we may extract it to a temp directory; track it so
// we can cleanup older temp directories when navigation changes. // we can cleanup older temp directories when navigation changes.
private Path currentArchiveTempDir = null; private Path currentArchiveTempDir = null;
@ -239,10 +242,14 @@ public class FilePanelTab extends JPanel {
repaint(); repaint();
} }
// Request focus on table even if clicking on empty area
fileTable.requestFocusInWindow();
// Single left-click should focus/select the item under cursor but // Single left-click should focus/select the item under cursor but
// should NOT toggle its marked state. This preserves keyboard // should NOT toggle its marked state. This preserves keyboard
// marking (Insert) while making mouse clicks act as simple focus. // marking (Insert) while making mouse clicks act as simple focus.
if (e.getClickCount() == 1 && javax.swing.SwingUtilities.isLeftMouseButton(e)) { if (e.getClickCount() == 1 && javax.swing.SwingUtilities.isLeftMouseButton(e)) {
boolean selected = false;
if (row >= 0) { if (row >= 0) {
// Convert brief layout coordinates to absolute index where needed // Convert brief layout coordinates to absolute index where needed
if (viewMode == ViewMode.BRIEF) { if (viewMode == ViewMode.BRIEF) {
@ -255,18 +262,24 @@ public class FilePanelTab extends JPanel {
briefCurrentColumn = selCol; briefCurrentColumn = selCol;
fileTable.setRowSelectionInterval(selRow, selRow); fileTable.setRowSelectionInterval(selRow, selRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true)); fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true));
selected = true;
} }
} }
} else { } else {
// FULL mode: rows map directly // FULL mode: rows map directly
fileTable.setRowSelectionInterval(row, row); fileTable.setRowSelectionInterval(row, row);
fileTable.scrollRectToVisible(fileTable.getCellRect(row, 0, true)); fileTable.scrollRectToVisible(fileTable.getCellRect(row, 0, true));
selected = true;
}
}
if (!selected) {
// Clicked on empty area (row < 0 or empty cell in BRIEF): select the last item in the panel
selectLastItem();
} }
fileTable.requestFocusInWindow();
repaint(); repaint();
updateStatus(); updateStatus();
} }
}
// Double-click opens the item under cursor (directories) // Double-click opens the item under cursor (directories)
if (e.getClickCount() == 2) { if (e.getClickCount() == 2) {
@ -281,9 +294,11 @@ public class FilePanelTab extends JPanel {
// Allow MOUSE_PRESSED for drag initiating gestures, but block standard selection change. // Allow MOUSE_PRESSED for drag initiating gestures, but block standard selection change.
// We'll process selection manually in MOUSE_CLICKED above. // We'll process selection manually in MOUSE_CLICKED above.
if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED) { if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED) {
fileTable.requestFocusInWindow();
// Start selection logic on press to support DnD initiate // Start selection logic on press to support DnD initiate
int col = columnAtPoint(e.getPoint()); int col = columnAtPoint(e.getPoint());
int row = rowAtPoint(e.getPoint()); int row = rowAtPoint(e.getPoint());
boolean selected = false;
if (row >= 0) { if (row >= 0) {
if (viewMode == ViewMode.BRIEF) { if (viewMode == ViewMode.BRIEF) {
FileItem item = tableModel.getItemFromBriefLayout(row, col); FileItem item = tableModel.getItemFromBriefLayout(row, col);
@ -293,14 +308,25 @@ public class FilePanelTab extends JPanel {
int selRow = index % tableModel.briefRowsPerColumn; int selRow = index % tableModel.briefRowsPerColumn;
fileTable.setRowSelectionInterval(selRow, selRow); fileTable.setRowSelectionInterval(selRow, selRow);
briefCurrentColumn = index / tableModel.briefRowsPerColumn; briefCurrentColumn = index / tableModel.briefRowsPerColumn;
selected = true;
} }
} }
} else { } else {
fileTable.setRowSelectionInterval(row, row); fileTable.setRowSelectionInterval(row, row);
selected = true;
}
}
if (!selected) {
// Clicked on empty area of the table: select the last item
selectLastItem();
// For empty area, we MUST consume the event to prevent JTable from clearing selection
e.consume();
return;
} }
fileTable.requestFocusInWindow();
repaint(); repaint();
} // If we are on a row, we let it pass to super so Drag and Drop can be initiated,
// but since we already set selection, JTable will just confirm it.
} }
if (e.getID() == java.awt.event.MouseEvent.MOUSE_RELEASED) { if (e.getID() == java.awt.event.MouseEvent.MOUSE_RELEASED) {
@ -308,8 +334,10 @@ public class FilePanelTab extends JPanel {
return; return;
} }
if (!e.isConsumed()) {
super.processMouseEvent(e); super.processMouseEvent(e);
} }
}
@Override @Override
protected void processMouseMotionEvent(java.awt.event.MouseEvent e) { protected void processMouseMotionEvent(java.awt.event.MouseEvent e) {
// Allow mouse movement to pass through to support Drag and Drop // Allow mouse movement to pass through to support Drag and Drop
@ -433,6 +461,40 @@ public class FilePanelTab extends JPanel {
fileTable.setBackground(this.getBackground()); fileTable.setBackground(this.getBackground());
fileTable.setOpaque(true); fileTable.setOpaque(true);
fileTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Enforce that at least one item is always active (selected) if the table is not empty.
fileTable.getSelectionModel().addListSelectionListener(e -> {
int row = fileTable.getSelectedRow();
if (row >= 0) {
if (!e.getValueIsAdjusting()) {
lastValidRow = row;
lastValidBriefColumn = briefCurrentColumn;
}
} else {
// Selection became empty. Attempt to restore it.
// We do this even if e.getValueIsAdjusting() is true to prevent temporary selection loss.
if (fileTable.getRowCount() > 0) {
int targetRow = Math.min(lastValidRow, fileTable.getRowCount() - 1);
if (targetRow < 0) targetRow = 0;
final int finalRow = targetRow;
final int finalCol = lastValidBriefColumn;
// Use invokeLater to avoid potential re-entrancy issues with selection model
SwingUtilities.invokeLater(() -> {
if (fileTable != null && fileTable.getSelectionModel().isSelectionEmpty() && fileTable.getRowCount() > 0) {
briefCurrentColumn = finalCol;
fileTable.setRowSelectionInterval(finalRow, finalRow);
// Ensure the restored selection is visible
try {
fileTable.scrollRectToVisible(fileTable.getCellRect(finalRow, finalCol, true));
} catch (Exception ignore) {}
}
});
}
}
});
JScrollPane scrollPane = new JScrollPane(fileTable); JScrollPane scrollPane = new JScrollPane(fileTable);
// Enable horizontal scrollbar when needed so BRIEF mode can scroll left-right // Enable horizontal scrollbar when needed so BRIEF mode can scroll left-right
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
@ -728,6 +790,8 @@ public class FilePanelTab extends JPanel {
this.currentDirectory = directory; this.currentDirectory = directory;
briefCurrentColumn = 0; briefCurrentColumn = 0;
lastValidRow = 0;
lastValidBriefColumn = 0;
File[] files = directory.listFiles(); File[] files = directory.listFiles();
List<FileItem> items = new ArrayList<>(); List<FileItem> items = new ArrayList<>();
@ -1294,6 +1358,28 @@ public class FilePanelTab extends JPanel {
return null; return null;
} }
/**
* Mark/Select the very last item in the list
*/
public void selectLastItem() {
int count = tableModel.getRowCount();
if (count > 0) {
if (viewMode == ViewMode.BRIEF) {
int lastIndex = tableModel.items.size() - 1;
briefCurrentColumn = lastIndex / tableModel.briefRowsPerColumn;
int row = lastIndex % tableModel.briefRowsPerColumn;
fileTable.setRowSelectionInterval(row, row);
fileTable.scrollRectToVisible(fileTable.getCellRect(row, briefCurrentColumn, true));
} else {
int last = count - 1;
fileTable.setRowSelectionInterval(last, last);
fileTable.scrollRectToVisible(fileTable.getCellRect(last, 0, true));
}
repaint();
updateStatus();
}
}
public void setViewMode(ViewMode mode) { public void setViewMode(ViewMode mode) {
if (this.viewMode != mode) { if (this.viewMode != mode) {
String selectedItemName = null; String selectedItemName = null;

View File

@ -144,20 +144,33 @@ public class MainWindow extends JFrame {
// ignore and keep default // ignore and keep default
} }
// Focus listeners to track active panel // Global focus listener to track which panel is active based on focused component
KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("permanentFocusOwner", evt -> {
Component focused = (Component) evt.getNewValue();
if (focused != null) {
if (SwingUtilities.isDescendingFrom(focused, leftPanel)) {
activePanel = leftPanel;
updateActivePanelBorder();
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
} else if (SwingUtilities.isDescendingFrom(focused, rightPanel)) {
activePanel = rightPanel;
updateActivePanelBorder();
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
}
}
});
// Focus listeners to track active panel and ensure selection
leftPanel.getFileTable().addFocusListener(new FocusAdapter() { leftPanel.getFileTable().addFocusListener(new FocusAdapter() {
@Override @Override
public void focusGained(FocusEvent e) { public void focusGained(FocusEvent e) {
activePanel = leftPanel;
updateActivePanelBorder();
// Ensure some row is selected // Ensure some row is selected
JTable leftTable = leftPanel.getFileTable(); JTable leftTable = leftPanel.getFileTable();
if (leftTable.getSelectedRow() == -1 && leftTable.getRowCount() > 0) { if (leftTable.getSelectedRow() == -1 && leftTable.getRowCount() > 0) {
leftTable.setRowSelectionInterval(0, 0); leftTable.setRowSelectionInterval(0, 0);
} }
// Repaint both panels
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
} }
@Override @Override
@ -170,16 +183,11 @@ public class MainWindow extends JFrame {
rightPanel.getFileTable().addFocusListener(new FocusAdapter() { rightPanel.getFileTable().addFocusListener(new FocusAdapter() {
@Override @Override
public void focusGained(FocusEvent e) { public void focusGained(FocusEvent e) {
activePanel = rightPanel;
updateActivePanelBorder();
// Ensure some row is selected // Ensure some row is selected
JTable rightTable = rightPanel.getFileTable(); JTable rightTable = rightPanel.getFileTable();
if (rightTable.getSelectedRow() == -1 && rightTable.getRowCount() > 0) { if (rightTable.getSelectedRow() == -1 && rightTable.getRowCount() > 0) {
rightTable.setRowSelectionInterval(0, 0); rightTable.setRowSelectionInterval(0, 0);
} }
// Repaint both panels
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
} }
@Override @Override