From f88a6c10a18a1df0ccdbb5cc3c8e7e446f03a935 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Fri, 16 Jan 2026 17:40:56 +0100 Subject: [PATCH] focus fixes --- src/main/java/com/kfmanager/ui/FilePanel.java | 42 ++++++++ .../java/com/kfmanager/ui/FilePanelTab.java | 100 ++++++++++++++++-- .../java/com/kfmanager/ui/MainWindow.java | 30 ++++-- 3 files changed, 154 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/kfmanager/ui/FilePanel.java b/src/main/java/com/kfmanager/ui/FilePanel.java index e638826..91ffc22 100644 --- a/src/main/java/com/kfmanager/ui/FilePanel.java +++ b/src/main/java/com/kfmanager/ui/FilePanel.java @@ -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(); diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java index e1840b1..b0f80d5 100644 --- a/src/main/java/com/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -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 items = new ArrayList<>(); @@ -1293,6 +1357,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) { diff --git a/src/main/java/com/kfmanager/ui/MainWindow.java b/src/main/java/com/kfmanager/ui/MainWindow.java index 37746f6..52e3add 100644 --- a/src/main/java/com/kfmanager/ui/MainWindow.java +++ b/src/main/java/com/kfmanager/ui/MainWindow.java @@ -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