diff --git a/src/main/java/cz/kamma/kfmanager/config/AppConfig.java b/src/main/java/cz/kamma/kfmanager/config/AppConfig.java index 34a9bb5..787e0cf 100644 --- a/src/main/java/cz/kamma/kfmanager/config/AppConfig.java +++ b/src/main/java/cz/kamma/kfmanager/config/AppConfig.java @@ -464,6 +464,14 @@ public class AppConfig { public void setIgnoreLeadingDot(boolean enabled) { properties.setProperty("global.sort.ignore.leadingdot", String.valueOf(enabled)); } + + public int getAutoRefreshInterval() { + return Integer.parseInt(properties.getProperty("panel.autoRefreshInterval", "2000")); + } + + public void setAutoRefreshInterval(int interval) { + properties.setProperty("panel.autoRefreshInterval", String.valueOf(interval)); + } // -- Multiple sort criteria persistence public java.util.List getMultipleSortCriteria() { diff --git a/src/main/java/cz/kamma/kfmanager/model/FileItem.java b/src/main/java/cz/kamma/kfmanager/model/FileItem.java index a67376a..b136657 100644 --- a/src/main/java/cz/kamma/kfmanager/model/FileItem.java +++ b/src/main/java/cz/kamma/kfmanager/model/FileItem.java @@ -69,6 +69,19 @@ public class FileItem { return isDirectory; } + /** + * Check if this item is the same as another item based on metadata. + */ + public boolean isSameAs(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + FileItem other = (FileItem) obj; + return isDirectory == other.isDirectory && + size == other.size && + (name != null ? name.equals(other.name) : other.name == null) && + (modified != null ? modified.getTime() == other.modified.getTime() : other.modified == null); + } + public Icon getIcon() { return icon; } diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java index 0666507..ec1f40c 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java @@ -717,6 +717,13 @@ public class FilePanel extends JPanel { updatePathField(); } } + + public void refresh(boolean requestFocus) { + FilePanelTab tab = getCurrentTab(); + if (tab != null) { + tab.refresh(requestFocus); + } + } public void toggleSelectionAndMoveDown() { FilePanelTab tab = getCurrentTab(); diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index 8844f6c..57808d1 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -861,29 +861,7 @@ public class FilePanelTab extends JPanel { lastValidRow = 0; lastValidBriefColumn = 0; - File[] files = directory.listFiles(); - List items = new ArrayList<>(); - - File parent = directory.getParentFile(); - if (parent != null) { - items.add(new FileItem(parent) { - @Override - public String getName() { - return ".."; - } - }); - } - - if (files != null && files.length > 0) { - Arrays.sort(files, Comparator - .comparing((File f) -> !f.isDirectory()) - .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER)); - - for (File file : files) { - items.add(new FileItem(file)); - } - } - + List items = createFileItemList(directory); tableModel.setItems(items); if (viewMode == ViewMode.BRIEF) { @@ -912,11 +890,11 @@ public class FilePanelTab extends JPanel { if (autoSelectFirst && fileTable.getRowCount() > 0) { int startIndex = 0; fileTable.setRowSelectionInterval(startIndex, startIndex); - if (requestFocus) { - SwingUtilities.invokeLater(() -> { - try { fileTable.requestFocusInWindow(); } catch (Exception ignore) {} - }); - } + } + if (requestFocus) { + SwingUtilities.invokeLater(() -> { + try { fileTable.requestFocusInWindow(); } catch (Exception ignore) {} + }); } } @@ -928,6 +906,94 @@ public class FilePanelTab extends JPanel { } } + /** + * Refresh the current directory while attempting to preserve selection and focus. + */ + public void refresh(boolean requestFocus) { + List newItems = createFileItemList(currentDirectory); + if (isSameContent(newItems, tableModel.items)) { + return; + } + + FileItem focused = getFocusedItem(); + final String focusedName = (focused != null) ? focused.getName() : null; + + final List markedNames = new ArrayList<>(); + for (FileItem item : tableModel.items) { + if (item.isMarked()) { + markedNames.add(item.getName()); + } + } + + loadDirectory(currentDirectory, false, requestFocus); + + SwingUtilities.invokeLater(() -> { + // Restore marks + for (FileItem item : tableModel.items) { + if (markedNames.contains(item.getName())) { + item.setMarked(true); + } + } + + // Restore focus + if (focusedName != null) { + for (int i = 0; i < tableModel.items.size(); i++) { + if (tableModel.items.get(i).getName().equals(focusedName)) { + int row = i; + if (viewMode == ViewMode.BRIEF) { + int selRow = row % tableModel.briefRowsPerColumn; + int selCol = row / tableModel.briefRowsPerColumn; + briefCurrentColumn = selCol; + fileTable.getSelectionModel().setSelectionInterval(selRow, selRow); + fileTable.getColumnModel().getSelectionModel().setSelectionInterval(selCol, selCol); + fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true)); + } else { + fileTable.getSelectionModel().setSelectionInterval(row, row); + fileTable.scrollRectToVisible(fileTable.getCellRect(row, 0, true)); + } + break; + } + } + } + fileTable.repaint(); + updateStatus(); + }); + } + + private List createFileItemList(File directory) { + if (directory == null || !directory.isDirectory()) { + return new ArrayList<>(); + } + File[] files = directory.listFiles(); + List items = new ArrayList<>(); + File parent = directory.getParentFile(); + if (parent != null) { + items.add(new FileItem(parent) { + @Override + public String getName() { + return ".."; + } + }); + } + if (files != null && files.length > 0) { + Arrays.sort(files, Comparator + .comparing((File f) -> !f.isDirectory()) + .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + for (File file : files) { + items.add(new FileItem(file)); + } + } + return items; + } + + private boolean isSameContent(List list1, List list2) { + if (list1.size() != list2.size()) return false; + for (int i = 0; i < list1.size(); i++) { + if (!list1.get(i).isSameAs(list2.get(i))) return false; + } + return true; + } + /** * Cleanup previous archive temp dir when navigating away from it. */ diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java index 8e11dd2..eb60140 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java +++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java @@ -28,6 +28,7 @@ public class MainWindow extends JFrame { private JComboBox commandLine; private JLabel cmdLabel; private AppConfig config; + private Timer autoRefreshTimer; public MainWindow() { super("KF Manager v" + MainApp.APP_VERSION); @@ -55,6 +56,14 @@ public class MainWindow extends JFrame { MainWindow.this.saveConfigAndExit(); } }); + + // Refresh panels when application gains focus + addWindowFocusListener(new WindowAdapter() { + @Override + public void windowGainedFocus(WindowEvent e) { + refreshPanels(); + } + }); // After start, set focus and selection to the active panel String initialActiveSide = config.getActivePanel(); @@ -69,6 +78,9 @@ public class MainWindow extends JFrame { updateActivePanelBorder(); updateCommandLinePrompt(); }); + + // Setup auto-refresh timer from config + updateAutoRefreshTimer(); } private void loadAppIcon() { @@ -399,6 +411,18 @@ public class MainWindow extends JFrame { toolBar.removeAll(); + // Refresh button + JButton btnRefresh = new JButton("↻"); + btnRefresh.setToolTipText("Refresh active panel"); + btnRefresh.setFocusable(false); + btnRefresh.addActionListener(e -> { + if (activePanel != null && activePanel.getCurrentDirectory() != null) { + activePanel.refresh(true); + } + }); + toolBar.add(btnRefresh); + toolBar.addSeparator(); + // Button for BRIEF mode JButton btnBrief = new JButton("☰ Brief"); btnBrief.setToolTipText("Brief mode - multiple columns (Ctrl+F1)"); @@ -725,6 +749,7 @@ public class MainWindow extends JFrame { * Apply appearance settings (font/colors) from config to UI components. */ private void applyAppearanceSettings() { + updateAutoRefreshTimer(); Font gfont = config.getGlobalFont(); if (gfont != null) { // Apply to toolbars, buttons and tables @@ -1749,16 +1774,14 @@ public class MainWindow extends JFrame { } /** - * Refresh both panels + * Refresh both panels while preserving selection and active panel focus. */ private void refreshPanels() { - // Refresh is now automatic upon changes - // If manual refresh is needed, we can call loadDirectory - if (leftPanel.getCurrentDirectory() != null) { - leftPanel.loadDirectory(leftPanel.getCurrentDirectory()); + if (leftPanel != null && leftPanel.getCurrentDirectory() != null) { + leftPanel.refresh(activePanel == leftPanel); } - if (rightPanel.getCurrentDirectory() != null) { - rightPanel.loadDirectory(rightPanel.getCurrentDirectory()); + if (rightPanel != null && rightPanel.getCurrentDirectory() != null) { + rightPanel.refresh(activePanel == rightPanel); } } @@ -2177,6 +2200,9 @@ public class MainWindow extends JFrame { * Save configuration and exit application */ private void saveConfigAndExit() { + if (autoRefreshTimer != null) { + autoRefreshTimer.stop(); + } // Save window state config.saveWindowState(this); @@ -2257,4 +2283,17 @@ public class MainWindow extends JFrame { activePanel.getFileTable().requestFocusInWindow(); } } + + private void updateAutoRefreshTimer() { + if (autoRefreshTimer != null) { + autoRefreshTimer.stop(); + } + int interval = config.getAutoRefreshInterval(); + autoRefreshTimer = new Timer(interval, e -> { + if (activePanel != null && activePanel.getCurrentDirectory() != null) { + activePanel.refresh(false); + } + }); + autoRefreshTimer.start(); + } } diff --git a/src/main/java/cz/kamma/kfmanager/ui/SettingsDialog.java b/src/main/java/cz/kamma/kfmanager/ui/SettingsDialog.java index 43ab52e..be1b693 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/SettingsDialog.java +++ b/src/main/java/cz/kamma/kfmanager/ui/SettingsDialog.java @@ -209,6 +209,8 @@ public class SettingsDialog extends JDialog { try { JSpinner mws = (JSpinner) behaviorHolder.getClientProperty("mouseWheelSteps"); if (mws != null) config.setBriefMouseWheelSteps((Integer) mws.getValue()); + JSpinner ari = (JSpinner) behaviorHolder.getClientProperty("autoRefreshInterval"); + if (ari != null) config.setAutoRefreshInterval((Integer) ari.getValue()); } catch (Exception ignore) {} } @@ -436,8 +438,17 @@ public class SettingsDialog extends JDialog { gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; grid.add(mwSteps, gbc); + // Auto-refresh interval + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("Auto-refresh interval (ms):"), gbc); + + JSpinner refreshInt = new JSpinner(new SpinnerNumberModel(config.getAutoRefreshInterval(), 100, 600000, 100)); + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(refreshInt, gbc); + p.add(grid, BorderLayout.NORTH); p.putClientProperty("mouseWheelSteps", mwSteps); + p.putClientProperty("autoRefreshInterval", refreshInt); panels.put("Behavior", p); return p; } diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png index cd1b6f9..ca059be 100644 Binary files a/src/main/resources/icon.png and b/src/main/resources/icon.png differ