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);
// 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.setSelectedComponent(tab);
// Ensure clicking on empty areas of the tab/scrollpane focuses the table
addMouseListenerToComponents(tab);
// Update path field
updatePathField();
updateTabStyles();
@ -225,6 +264,9 @@ public class FilePanel extends JPanel {
tabbedPane.addTab(tabTitle, tab);
tabbedPane.setSelectedComponent(tab);
// Ensure clicking on empty areas of the tab/scrollpane focuses the table
addMouseListenerToComponents(tab);
updatePathField();
updateTabStyles();

View File

@ -42,7 +42,10 @@ public class FilePanelTab extends JPanel {
// Sorting state for FULL mode header clicks
private int sortColumn = -1; // 0=name,1=size,2=date
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
// we can cleanup older temp directories when navigation changes.
private Path currentArchiveTempDir = null;
@ -239,10 +242,14 @@ public class FilePanelTab extends JPanel {
repaint();
}
// Request focus on table even if clicking on empty area
fileTable.requestFocusInWindow();
// Single left-click should focus/select the item under cursor but
// should NOT toggle its marked state. This preserves keyboard
// marking (Insert) while making mouse clicks act as simple focus.
if (e.getClickCount() == 1 && javax.swing.SwingUtilities.isLeftMouseButton(e)) {
boolean selected = false;
if (row >= 0) {
// Convert brief layout coordinates to absolute index where needed
if (viewMode == ViewMode.BRIEF) {
@ -255,17 +262,23 @@ public class FilePanelTab extends JPanel {
briefCurrentColumn = selCol;
fileTable.setRowSelectionInterval(selRow, selRow);
fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true));
selected = true;
}
}
} else {
// FULL mode: rows map directly
fileTable.setRowSelectionInterval(row, row);
fileTable.scrollRectToVisible(fileTable.getCellRect(row, 0, true));
selected = true;
}
fileTable.requestFocusInWindow();
repaint();
updateStatus();
}
if (!selected) {
// Clicked on empty area (row < 0 or empty cell in BRIEF): select the last item in the panel
selectLastItem();
}
repaint();
updateStatus();
}
// Double-click opens the item under cursor (directories)
@ -281,9 +294,11 @@ public class FilePanelTab extends JPanel {
// Allow MOUSE_PRESSED for drag initiating gestures, but block standard selection change.
// We'll process selection manually in MOUSE_CLICKED above.
if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED) {
fileTable.requestFocusInWindow();
// Start selection logic on press to support DnD initiate
int col = columnAtPoint(e.getPoint());
int row = rowAtPoint(e.getPoint());
boolean selected = false;
if (row >= 0) {
if (viewMode == ViewMode.BRIEF) {
FileItem item = tableModel.getItemFromBriefLayout(row, col);
@ -293,14 +308,25 @@ public class FilePanelTab extends JPanel {
int selRow = index % tableModel.briefRowsPerColumn;
fileTable.setRowSelectionInterval(selRow, selRow);
briefCurrentColumn = index / tableModel.briefRowsPerColumn;
selected = true;
}
}
} else {
fileTable.setRowSelectionInterval(row, row);
selected = true;
}
fileTable.requestFocusInWindow();
repaint();
}
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;
}
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) {
@ -308,7 +334,9 @@ public class FilePanelTab extends JPanel {
return;
}
super.processMouseEvent(e);
if (!e.isConsumed()) {
super.processMouseEvent(e);
}
}
@Override
protected void processMouseMotionEvent(java.awt.event.MouseEvent e) {
@ -433,6 +461,40 @@ public class FilePanelTab extends JPanel {
fileTable.setBackground(this.getBackground());
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);
// Enable horizontal scrollbar when needed so BRIEF mode can scroll left-right
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
@ -728,6 +790,8 @@ public class FilePanelTab extends JPanel {
this.currentDirectory = directory;
briefCurrentColumn = 0;
lastValidRow = 0;
lastValidBriefColumn = 0;
File[] files = directory.listFiles();
List<FileItem> items = new ArrayList<>();
@ -1294,6 +1358,28 @@ public class FilePanelTab extends JPanel {
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) {
if (this.viewMode != mode) {
String selectedItemName = null;

View File

@ -144,20 +144,33 @@ public class MainWindow extends JFrame {
// 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() {
@Override
public void focusGained(FocusEvent e) {
activePanel = leftPanel;
updateActivePanelBorder();
// Ensure some row is selected
JTable leftTable = leftPanel.getFileTable();
if (leftTable.getSelectedRow() == -1 && leftTable.getRowCount() > 0) {
leftTable.setRowSelectionInterval(0, 0);
}
// Repaint both panels
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
}
@Override
@ -170,16 +183,11 @@ public class MainWindow extends JFrame {
rightPanel.getFileTable().addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
activePanel = rightPanel;
updateActivePanelBorder();
// Ensure some row is selected
JTable rightTable = rightPanel.getFileTable();
if (rightTable.getSelectedRow() == -1 && rightTable.getRowCount() > 0) {
rightTable.setRowSelectionInterval(0, 0);
}
// Repaint both panels
leftPanel.getFileTable().repaint();
rightPanel.getFileTable().repaint();
}
@Override