diff --git a/src/main/java/com/kfmanager/config/AppConfig.java b/src/main/java/com/kfmanager/config/AppConfig.java index 10fcac8..cfb8605 100644 --- a/src/main/java/com/kfmanager/config/AppConfig.java +++ b/src/main/java/com/kfmanager/config/AppConfig.java @@ -334,4 +334,109 @@ public class AppConfig { frame.setExtendedState(Frame.MAXIMIZED_BOTH); } } + + // --- Search dialog persistence --- + public int getSearchDialogX() { + return Integer.parseInt(properties.getProperty("searchDialog.x", "100")); + } + + public void setSearchDialogX(int x) { + properties.setProperty("searchDialog.x", String.valueOf(x)); + } + + public int getSearchDialogY() { + return Integer.parseInt(properties.getProperty("searchDialog.y", "100")); + } + + public void setSearchDialogY(int y) { + properties.setProperty("searchDialog.y", String.valueOf(y)); + } + + public int getSearchDialogWidth() { + return Integer.parseInt(properties.getProperty("searchDialog.width", "700")); + } + + public void setSearchDialogWidth(int w) { + properties.setProperty("searchDialog.width", String.valueOf(w)); + } + + public int getSearchDialogHeight() { + return Integer.parseInt(properties.getProperty("searchDialog.height", "500")); + } + + public void setSearchDialogHeight(int h) { + properties.setProperty("searchDialog.height", String.valueOf(h)); + } + + /** Save search dialog bounds */ + public void saveSearchDialogState(Window win) { + if (win == null) return; + setSearchDialogX(win.getX()); + setSearchDialogY(win.getY()); + setSearchDialogWidth(win.getWidth()); + setSearchDialogHeight(win.getHeight()); + } + + /** Restore search dialog bounds */ + public void restoreSearchDialogState(Window win) { + if (win == null) return; + win.setLocation(getSearchDialogX(), getSearchDialogY()); + win.setSize(getSearchDialogWidth(), getSearchDialogHeight()); + } + + // --- Search history persistence --- + public java.util.List getSearchHistory() { + java.util.List list = new java.util.ArrayList<>(); + int count = Integer.parseInt(properties.getProperty("search.history.count", "0")); + for (int i = 0; i < count; i++) { + String v = properties.getProperty("search.history." + i, null); + if (v != null && !v.isEmpty()) list.add(v); + } + return list; + } + + public void saveSearchHistory(java.util.List history) { + if (history == null) { + properties.setProperty("search.history.count", "0"); + return; + } + int limit = Math.min(history.size(), 50); // cap stored entries + properties.setProperty("search.history.count", String.valueOf(limit)); + for (int i = 0; i < limit; i++) { + properties.setProperty("search.history." + i, history.get(i)); + } + // remove any old entries beyond limit + int old = Integer.parseInt(properties.getProperty("search.history.count", "0")); + for (int i = limit; i < old; i++) { + properties.remove("search.history." + i); + } + } + + // --- Content search history persistence --- + public java.util.List getContentSearchHistory() { + java.util.List list = new java.util.ArrayList<>(); + int count = Integer.parseInt(properties.getProperty("search.content.history.count", "0")); + for (int i = 0; i < count; i++) { + String v = properties.getProperty("search.content.history." + i, null); + if (v != null && !v.isEmpty()) list.add(v); + } + return list; + } + + public void saveContentSearchHistory(java.util.List history) { + if (history == null) { + properties.setProperty("search.content.history.count", "0"); + return; + } + int limit = Math.min(history.size(), 50); + properties.setProperty("search.content.history.count", String.valueOf(limit)); + for (int i = 0; i < limit; i++) { + properties.setProperty("search.content.history." + i, history.get(i)); + } + // remove old entries beyond limit + int old = Integer.parseInt(properties.getProperty("search.content.history.count", "0")); + for (int i = limit; i < old; i++) { + properties.remove("search.content.history." + i); + } + } } diff --git a/src/main/java/com/kfmanager/service/FileOperations.java b/src/main/java/com/kfmanager/service/FileOperations.java index f62cd53..f3a05e3 100644 --- a/src/main/java/com/kfmanager/service/FileOperations.java +++ b/src/main/java/com/kfmanager/service/FileOperations.java @@ -6,6 +6,8 @@ import java.io.*; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Service for file operations - copy, move, delete, etc. @@ -156,19 +158,47 @@ public class FileOperations { * Search files by pattern */ public static void search(File directory, String pattern, boolean recursive, SearchCallback callback) throws IOException { - searchRecursive(directory.toPath(), pattern.toLowerCase(), recursive, callback); + if (pattern == null) return; + // Prepare a compiled regex if the pattern contains wildcards to avoid recompiling per-file + Pattern filenameRegex = null; + if (pattern.contains("*") || pattern.contains("?")) { + String regex = pattern + .replace(".", "\\.") + .replace("*", ".*") + .replace("?", "."); + filenameRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + searchRecursive(directory.toPath(), pattern.toLowerCase(), filenameRegex, recursive, callback); + } + + /** + * Search file contents for a text fragment (case-insensitive). + * Calls callback.onFileFound(file) when a file contains the text. + */ + public static void searchContents(File directory, String text, boolean recursive, SearchCallback callback) throws IOException { + if (text == null) return; + // Precompile a case-insensitive pattern for content search to avoid per-line lowercasing + Pattern contentPattern = Pattern.compile(Pattern.quote(text), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + searchContentsRecursive(directory.toPath(), contentPattern, recursive, callback); } - private static void searchRecursive(Path directory, String pattern, boolean recursive, SearchCallback callback) throws IOException { + private static void searchRecursive(Path directory, String patternLower, Pattern filenameRegex, boolean recursive, SearchCallback callback) throws IOException { try (DirectoryStream stream = Files.newDirectoryStream(directory)) { for (Path entry : stream) { if (Files.isDirectory(entry)) { if (recursive) { - searchRecursive(entry, pattern, recursive, callback); + searchRecursive(entry, patternLower, filenameRegex, recursive, callback); } } else { - String fileName = entry.getFileName().toString().toLowerCase(); - if (fileName.contains(pattern) || matchesPattern(fileName, pattern)) { + String fileName = entry.getFileName().toString(); + String fileNameLower = fileName.toLowerCase(); + boolean matched = false; + if (fileNameLower.contains(patternLower)) matched = true; + else if (filenameRegex != null) { + Matcher m = filenameRegex.matcher(fileName); + if (m.matches()) matched = true; + } + if (matched) { callback.onFileFound(entry.toFile()); } } @@ -177,18 +207,38 @@ public class FileOperations { // Ignore directories without access } } - - /** - * Check whether filename matches the pattern (supports * and ?) - */ - private static boolean matchesPattern(String fileName, String pattern) { - String regex = pattern - .replace(".", "\\.") - .replace("*", ".*") - .replace("?", "."); - return fileName.matches(regex); + + private static void searchContentsRecursive(Path directory, Pattern contentPattern, boolean recursive, SearchCallback callback) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(directory)) { + for (Path entry : stream) { + if (Files.isDirectory(entry)) { + if (recursive) { + searchContentsRecursive(entry, contentPattern, recursive, callback); + } + } else { + // Try reading file as text line-by-line and search for pattern (case-insensitive via compiled Pattern) + try (BufferedReader br = Files.newBufferedReader(entry)) { + String line; + boolean found = false; + while ((line = br.readLine()) != null) { + if (contentPattern.matcher(line).find()) { + found = true; + break; + } + } + if (found) callback.onFileFound(entry.toFile()); + } catch (IOException ex) { + // Skip files that cannot be read as text + } + } + } + } catch (AccessDeniedException e) { + // Ignore directories without access + } } + // legacy matchesPattern removed — filename wildcard handling is done via a precompiled Pattern + /** * Callback pro progress operací */ diff --git a/src/main/java/com/kfmanager/ui/FileEditor.java b/src/main/java/com/kfmanager/ui/FileEditor.java index c0cd74a..ccdc88e 100644 --- a/src/main/java/com/kfmanager/ui/FileEditor.java +++ b/src/main/java/com/kfmanager/ui/FileEditor.java @@ -311,6 +311,23 @@ public class FileEditor extends JDialog { } // Ensure byteTextOffsets length equals fileBytes length // If some bytes were missing due to empty file, leave as is + // After building the view text, position caret to the start of the rendered data + SwingUtilities.invokeLater(() -> { + try { + if (byteTextOffsets != null && !byteTextOffsets.isEmpty()) { + int pos = Math.max(0, byteTextOffsets.get(0)); + textArea.setCaretPosition(pos); + // ensure the caret is visible at top-left + Rectangle vis = textArea.getVisibleRect(); + Rectangle r = textArea.modelToView2D(pos).getBounds(); + if (r != null) { + textArea.scrollRectToVisible(new Rectangle(r.x, r.y, vis.width, vis.height)); + } + } else { + textArea.setCaretPosition(0); + } + } catch (Exception ignore) {} + }); } private void loadHexPage() { @@ -607,17 +624,14 @@ public class FileEditor extends JDialog { // makes the displayed offset follow scrolling (PgUp/PgDn or scrollbar) // rather than only caret movement. int refPos; - if (raf != null) { try { Rectangle vis = textArea.getVisibleRect(); Point topLeft = new Point(vis.x, vis.y); refPos = textArea.viewToModel2D(topLeft); } catch (Exception ex) { + // fallback to caret position if viewToModel fails refPos = textArea.getCaretPosition(); } - } else { - refPos = textArea.getCaretPosition(); - } int caret = refPos; long byteIndex = mapCaretToByteIndex(caret); long totalBytes = (raf != null && fileLength > 0) ? fileLength : (fileBytes != null ? fileBytes.length : 0); diff --git a/src/main/java/com/kfmanager/ui/FilePanel.java b/src/main/java/com/kfmanager/ui/FilePanel.java index 8e2e792..60022c0 100644 --- a/src/main/java/com/kfmanager/ui/FilePanel.java +++ b/src/main/java/com/kfmanager/ui/FilePanel.java @@ -413,7 +413,7 @@ public class FilePanel extends JPanel { long free = drive.getUsableSpace(); String freeGb = formatGbShort(free); String totalGb = formatGbShort(total); - String info = String.format("%s %s free of %s GB", name, freeGb, totalGb); + String info = String.format("%s %s GB free of %s GB", name, freeGb, totalGb); driveInfoLabel.setText(info); } catch (Exception ex) { driveInfoLabel.setText(""); diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java index 990d7fb..46e7e0c 100644 --- a/src/main/java/com/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -861,6 +861,14 @@ public class FilePanelTab extends JPanel { } } } + + /** + * Public wrapper to select an item by name from outside this class. + * Useful for other UI components to request focusing a specific file. + */ + public void selectItem(String name) { + selectItemByName(name); + } public void toggleSelectionAndMoveDown() { int selectedRow = fileTable.getSelectedRow(); diff --git a/src/main/java/com/kfmanager/ui/MainWindow.java b/src/main/java/com/kfmanager/ui/MainWindow.java index 6f06623..dcdf43c 100644 --- a/src/main/java/com/kfmanager/ui/MainWindow.java +++ b/src/main/java/com/kfmanager/ui/MainWindow.java @@ -453,9 +453,9 @@ public class MainWindow extends JFrame { KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.ALT_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); - // Ctrl+F - Search + // Alt+F7 - Search (changed from Ctrl+F) rootPane.registerKeyboardAction(e -> showSearchDialog(), - KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK), + KeyStroke.getKeyStroke(KeyEvent.VK_F7, InputEvent.ALT_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); // Ctrl+F1 - Full details @@ -704,9 +704,55 @@ public class MainWindow extends JFrame { * Show search dialog */ private void showSearchDialog() { - SearchDialog dialog = new SearchDialog(this, activePanel.getCurrentDirectory()); + SearchDialog dialog = new SearchDialog(this, activePanel.getCurrentDirectory(), config); dialog.setVisible(true); } + + /** + * Show the given file's parent directory in the panel that currently has focus + * and select the file in that panel. + */ + public void showFileInFocusedPanel(File file) { + if (file == null) return; + // Determine which panel currently has focus + java.awt.Component owner = java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); + FilePanel target = null; + if (owner != null) { + Component p = owner; + while (p != null) { + if (p == leftPanel) { + target = leftPanel; + break; + } + if (p == rightPanel) { + target = rightPanel; + break; + } + p = p.getParent(); + } + } + if (target == null) target = activePanel != null ? activePanel : leftPanel; + + final FilePanel chosen = target; + final File parentDir = file.getParentFile(); + if (parentDir == null) return; + + // Load directory and then select item by name on the EDT + SwingUtilities.invokeLater(() -> { + chosen.loadDirectory(parentDir); + // mark this panel active and refresh borders + activePanel = chosen; + updateActivePanelBorder(); + // After loading, select the file name + SwingUtilities.invokeLater(() -> { + FilePanelTab tab = chosen.getCurrentTab(); + if (tab != null) { + tab.selectItem(file.getName()); + tab.getFileTable().requestFocusInWindow(); + } + }); + }); + } /** * Show file in internal viewer diff --git a/src/main/java/com/kfmanager/ui/SearchDialog.java b/src/main/java/com/kfmanager/ui/SearchDialog.java index 0fe949a..b6b9a7a 100644 --- a/src/main/java/com/kfmanager/ui/SearchDialog.java +++ b/src/main/java/com/kfmanager/ui/SearchDialog.java @@ -4,9 +4,12 @@ import com.kfmanager.model.FileItem; import com.kfmanager.service.FileOperations; import javax.swing.*; +import java.awt.event.KeyEvent; +import java.awt.event.InputEvent; import javax.swing.table.AbstractTableModel; import java.awt.*; import java.io.File; +import java.awt.Desktop; import java.util.ArrayList; import java.util.List; @@ -15,21 +18,45 @@ import java.util.List; */ public class SearchDialog extends JDialog { - private JTextField patternField; + private JComboBox patternCombo; + private JComboBox contentPatternCombo; private JCheckBox recursiveCheckBox; + private JCheckBox contentSearchCheckBox; private JTable resultsTable; private ResultsTableModel tableModel; private JButton searchButton; private JButton cancelButton; + private JButton viewButton; + private JButton editButton; + private JProgressBar statusProgressBar; + private JLabel statusLabel; + private volatile int foundCount = 0; private File searchDirectory; private volatile boolean searching = false; + private com.kfmanager.config.AppConfig config; - public SearchDialog(Frame parent, File searchDirectory) { - super(parent, "Search files", true); + public SearchDialog(Frame parent, File searchDirectory, com.kfmanager.config.AppConfig config) { + // Make the dialog modeless so it does not remain forced above other windows + super(parent, "Search files", false); this.searchDirectory = searchDirectory; + this.config = config; initComponents(); - setSize(700, 500); - setLocationRelativeTo(parent); + // Allow the user to resize the search dialog + setResizable(true); + // sensible minimum size so layout remains usable + setMinimumSize(new Dimension(480, 320)); + // Restore previous bounds if config present + if (this.config != null) { + try { + this.config.restoreSearchDialogState(this); + } catch (Exception ignore) { + setSize(700, 500); + setLocationRelativeTo(parent); + } + } else { + setSize(700, 500); + setLocationRelativeTo(parent); + } } private void initComponents() { @@ -48,18 +75,51 @@ public class SearchDialog extends JDialog { gbc.gridx = 1; gbc.weightx = 1.0; - patternField = new JTextField(); - patternField.setToolTipText("Enter filename or pattern (* for any chars, ? for single char)"); - searchPanel.add(patternField, gbc); + // Pattern input is an editable combo box populated from history + java.util.List history = config != null ? config.getSearchHistory() : java.util.Collections.emptyList(); + javax.swing.DefaultComboBoxModel historyModel = new javax.swing.DefaultComboBoxModel<>(); + for (String h : history) historyModel.addElement(h); + patternCombo = new JComboBox<>(historyModel); + patternCombo.setEditable(true); + // start with an empty editor so the field is blank on open + try { + patternCombo.setSelectedItem(""); + patternCombo.getEditor().setItem(""); + } catch (Exception ignore) {} + patternCombo.setToolTipText("Enter filename or pattern (* for any chars, ? for single char)"); + searchPanel.add(patternCombo, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 2; - recursiveCheckBox = new JCheckBox("Include subdirectories", true); + recursiveCheckBox = new JCheckBox("Include subdirectories", true); searchPanel.add(recursiveCheckBox, gbc); - + + // Row for content text pattern + gbc.gridx = 0; gbc.gridy = 2; - JLabel pathLabel = new JLabel("Directory: " + searchDirectory.getAbsolutePath()); + gbc.gridwidth = 1; + searchPanel.add(new JLabel("Text:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + java.util.List contentHistory = config != null ? config.getContentSearchHistory() : java.util.Collections.emptyList(); + javax.swing.DefaultComboBoxModel contentHistoryModel = new javax.swing.DefaultComboBoxModel<>(); + for (String h : contentHistory) contentHistoryModel.addElement(h); + contentPatternCombo = new JComboBox<>(contentHistoryModel); + contentPatternCombo.setEditable(true); + try { contentPatternCombo.setSelectedItem(""); contentPatternCombo.getEditor().setItem(""); } catch (Exception ignore) {} + contentPatternCombo.setToolTipText("Text to search inside files"); + searchPanel.add(contentPatternCombo, gbc); + + gbc.gridx = 0; + gbc.gridy = 3; + gbc.gridwidth = 2; + contentSearchCheckBox = new JCheckBox("Search inside file contents", false); + searchPanel.add(contentSearchCheckBox, gbc); + + gbc.gridy = 4; + JLabel pathLabel = new JLabel("Directory: " + searchDirectory.getAbsolutePath()); pathLabel.setFont(pathLabel.getFont().deriveFont(Font.ITALIC)); searchPanel.add(pathLabel, gbc); @@ -70,6 +130,8 @@ public class SearchDialog extends JDialog { resultsTable = new JTable(tableModel); resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); resultsTable.setFont(new Font("Monospaced", Font.PLAIN, 12)); + // let the table columns adjust when the dialog is resized + resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS); resultsTable.getColumnModel().getColumn(0).setPreferredWidth(400); resultsTable.getColumnModel().getColumn(1).setPreferredWidth(100); @@ -84,61 +146,315 @@ public class SearchDialog extends JDialog { } } }); + + // Enable/disable view/edit buttons depending on selection + resultsTable.getSelectionModel().addListSelectionListener(e -> { + int sel = resultsTable.getSelectedRow(); + boolean ok = false; + if (sel >= 0) { + FileItem it = tableModel.getResult(sel); + ok = it != null && !it.isDirectory() && !"..".equals(it.getName()); + } + viewButton.setEnabled(ok); + editButton.setEnabled(ok); + }); - JScrollPane scrollPane = new JScrollPane(resultsTable); - scrollPane.setBorder(BorderFactory.createTitledBorder("Výsledky")); - add(scrollPane, BorderLayout.CENTER); + JScrollPane scrollPane = new JScrollPane(resultsTable); + scrollPane.setBorder(BorderFactory.createTitledBorder("Výsledky")); + add(scrollPane, BorderLayout.CENTER); - // Panel s tlačítky - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + // Status bar with progress and message + statusLabel = new JLabel("Ready"); + statusProgressBar = new JProgressBar(); + statusProgressBar.setVisible(false); + statusProgressBar.setIndeterminate(false); + JPanel statusPanel = new JPanel(new BorderLayout(6, 6)); + statusPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + statusPanel.add(statusLabel, BorderLayout.CENTER); + statusPanel.add(statusProgressBar, BorderLayout.EAST); + + // Panel s tlačítky + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - searchButton = new JButton("Hledat"); - searchButton.addActionListener(e -> performSearch()); + searchButton = new JButton("Hledat"); + searchButton.addActionListener(e -> performSearch()); + + viewButton = new JButton("Zobrazit"); + viewButton.setEnabled(false); + viewButton.addActionListener(e -> viewSelectedFile()); + + editButton = new JButton("Upravit"); + editButton.setEnabled(false); + editButton.addActionListener(e -> editSelectedFile()); + + cancelButton = new JButton("Zavřít"); + cancelButton.addActionListener(e -> dispose()); + + JButton openButton = new JButton("Otevřít umístění"); + openButton.addActionListener(e -> openSelectedFile()); + + buttonPanel.add(searchButton); + buttonPanel.add(viewButton); + buttonPanel.add(editButton); + buttonPanel.add(openButton); + buttonPanel.add(cancelButton); - cancelButton = new JButton("Zavřít"); - cancelButton.addActionListener(e -> dispose()); + // Compose bottom area: status bar above buttons + JPanel bottomPanel = new JPanel(new BorderLayout()); + bottomPanel.add(statusPanel, BorderLayout.NORTH); + bottomPanel.add(buttonPanel, BorderLayout.SOUTH); + add(bottomPanel, BorderLayout.SOUTH); - JButton openButton = new JButton("Otevřít umístění"); - openButton.addActionListener(e -> openSelectedFile()); - - buttonPanel.add(searchButton); - buttonPanel.add(openButton); - buttonPanel.add(cancelButton); - - add(buttonPanel, BorderLayout.SOUTH); - - // Enter pro spuštění hledání - patternField.addActionListener(e -> performSearch()); + // Require explicit Enter to start search when choosing from history. + // Bind Enter on the combo editor component so selecting an item from the + // popup does not automatically trigger search until user confirms. + java.awt.Component editorComp = patternCombo.getEditor().getEditorComponent(); + if (editorComp instanceof javax.swing.text.JTextComponent) { + javax.swing.text.JTextComponent tc = (javax.swing.text.JTextComponent) editorComp; + tc.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "confirmSearch"); + tc.getActionMap().put("confirmSearch", new AbstractAction() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + try { + // If the popup is visible, treat Enter as "confirm selection": fill editor and close popup + if (patternCombo.isPopupVisible()) { + Object sel = patternCombo.getSelectedItem(); + if (sel != null) { + patternCombo.getEditor().setItem(sel.toString()); + } + patternCombo.hidePopup(); + return; // do not start search yet + } + } catch (Exception ignore) {} + // Popup not visible -> actual confirm to start search + performSearch(); + } + }); + } + + // Same explicit-Enter behavior for content text pattern combo + try { + java.awt.Component contentEditorComp = contentPatternCombo.getEditor().getEditorComponent(); + if (contentEditorComp instanceof javax.swing.text.JTextComponent) { + javax.swing.text.JTextComponent tc2 = (javax.swing.text.JTextComponent) contentEditorComp; + tc2.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "confirmSearchContent"); + tc2.getActionMap().put("confirmSearchContent", new AbstractAction() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + try { + if (contentPatternCombo.isPopupVisible()) { + Object sel = contentPatternCombo.getSelectedItem(); + if (sel != null) contentPatternCombo.getEditor().setItem(sel.toString()); + contentPatternCombo.hidePopup(); + return; + } + } catch (Exception ignore) {} + performSearch(); + } + }); + } + } catch (Exception ignore) {} + + // Alt+F focuses filename (pattern) input; Alt+T focuses content text input + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.ALT_DOWN_MASK), "focusFilename"); + getRootPane().getActionMap().put("focusFilename", new AbstractAction() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + try { + java.awt.Component ed = patternCombo.getEditor().getEditorComponent(); + if (ed != null) { + ed.requestFocusInWindow(); + if (ed instanceof javax.swing.text.JTextComponent) { + ((javax.swing.text.JTextComponent) ed).selectAll(); + } + } + } catch (Exception ignore) {} + } + }); + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.ALT_DOWN_MASK), "focusContentText"); + getRootPane().getActionMap().put("focusContentText", new AbstractAction() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + try { + // Toggle the content-search checkbox (on/off) + try { + if (contentSearchCheckBox != null) { + contentSearchCheckBox.setSelected(!contentSearchCheckBox.isSelected()); + } + } catch (Exception ignore) {} + + java.awt.Component ed = contentPatternCombo.getEditor().getEditorComponent(); + if (ed != null) { + ed.requestFocusInWindow(); + if (ed instanceof javax.swing.text.JTextComponent) { + ((javax.swing.text.JTextComponent) ed).selectAll(); + } + } + } catch (Exception ignore) {} + } + }); + + // Escape closes the dialog (and cancels ongoing search) + getRootPane().registerKeyboardAction(e -> { + searching = false; + dispose(); + }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + // F3 = view, F4 = edit when dialog is active + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "viewResult"); + getRootPane().getActionMap().put("viewResult", new AbstractAction() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + viewSelectedFile(); + } + }); + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0), "editResult"); + getRootPane().getActionMap().put("editResult", new AbstractAction() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + editSelectedFile(); + } + }); + } + + @Override + public void dispose() { + // Persist bounds before disposing + try { + if (config != null) { + // also persist current pattern into history + try { + Object it = patternCombo != null ? patternCombo.getEditor().getItem() : null; + String cur = it != null ? it.toString().trim() : null; + if (cur != null && !cur.isEmpty()) { + java.util.List hist = new java.util.ArrayList<>(config.getSearchHistory()); + hist.remove(cur); + hist.add(0, cur); + int max = 20; + while (hist.size() > max) hist.remove(hist.size() - 1); + config.saveSearchHistory(hist); + } + } catch (Exception ignore) {} + // also persist current content search pattern into history + try { + Object cit = contentPatternCombo != null ? contentPatternCombo.getEditor().getItem() : null; + String ccur = cit != null ? cit.toString().trim() : null; + if (ccur != null && !ccur.isEmpty()) { + java.util.List chist = new java.util.ArrayList<>(config.getContentSearchHistory()); + chist.remove(ccur); + chist.add(0, ccur); + int maxc = 20; + while (chist.size() > maxc) chist.remove(chist.size() - 1); + config.saveContentSearchHistory(chist); + } + } catch (Exception ignore) {} + config.saveSearchDialogState(this); + config.saveConfig(); + } + } catch (Exception ignore) {} + super.dispose(); } /** * Provede vyhledávání */ private void performSearch() { - String pattern = patternField.getText().trim(); - if (pattern.isEmpty()) { - JOptionPane.showMessageDialog(this, - "Zadejte hledaný vzor", - "Chyba", - JOptionPane.WARNING_MESSAGE); - return; + String namePat = ""; + try { + Object it = patternCombo.getEditor().getItem(); + namePat = it != null ? it.toString().trim() : ""; + } catch (Exception ex) { namePat = ""; } + + String contentPat = ""; + try { + Object cit = contentPatternCombo.getEditor().getItem(); + contentPat = cit != null ? cit.toString().trim() : ""; + } catch (Exception ex) { contentPat = ""; } + + final boolean isContentSearch = contentSearchCheckBox != null && contentSearchCheckBox.isSelected(); + + if (isContentSearch) { + if (contentPat.isEmpty()) { + JOptionPane.showMessageDialog(this, + "Zadejte hledaný text", + "Chyba", + JOptionPane.WARNING_MESSAGE); + return; + } + } else { + if (namePat.isEmpty()) { + JOptionPane.showMessageDialog(this, + "Zadejte hledaný vzor", + "Chyba", + JOptionPane.WARNING_MESSAGE); + return; + } } - + tableModel.clear(); searchButton.setEnabled(false); searching = true; - - // Spustit vyhledávání v samostatném vlákně + + // Persist the chosen pattern into the appropriate history (most-recent-first) + if (config != null) { + try { + if (isContentSearch) { + java.util.List chist = new java.util.ArrayList<>(config.getContentSearchHistory()); + chist.remove(contentPat); + chist.add(0, contentPat); + int max = 20; + while (chist.size() > max) chist.remove(chist.size() - 1); + config.saveContentSearchHistory(chist); + config.saveConfig(); + // update content combo model + javax.swing.DefaultComboBoxModel cm = (javax.swing.DefaultComboBoxModel) contentPatternCombo.getModel(); + cm.removeAllElements(); + for (String s : chist) cm.addElement(s); + contentPatternCombo.setSelectedItem(contentPat); + } else { + java.util.List hist = new java.util.ArrayList<>(config.getSearchHistory()); + hist.remove(namePat); + hist.add(0, namePat); + int max = 20; + while (hist.size() > max) hist.remove(hist.size() - 1); + config.saveSearchHistory(hist); + config.saveConfig(); + // update combo model + javax.swing.DefaultComboBoxModel m = (javax.swing.DefaultComboBoxModel) patternCombo.getModel(); + m.removeAllElements(); + for (String s : hist) m.addElement(s); + patternCombo.setSelectedItem(namePat); + } + } catch (Exception ignore) {} + } + + final String pattern = isContentSearch ? contentPat : namePat; + + // Reset and show status + foundCount = 0; + statusLabel.setText("Searching..."); + statusProgressBar.setVisible(true); + statusProgressBar.setIndeterminate(true); + + // Spustit vyhledávání v samostatném vlákně SwingWorker worker = new SwingWorker() { @Override protected Void doInBackground() throws Exception { - FileOperations.search(searchDirectory, pattern, recursiveCheckBox.isSelected(), - file -> { - if (!searching) { - return; - } - publish(file); - }); + if (isContentSearch) { + FileOperations.searchContents(searchDirectory, pattern, recursiveCheckBox.isSelected(), file -> { + if (!searching) return; + publish(file); + }); + } else { + FileOperations.search(searchDirectory, pattern, recursiveCheckBox.isSelected(), file -> { + if (!searching) return; + publish(file); + }); + } return null; } @@ -146,6 +462,16 @@ public class SearchDialog extends JDialog { protected void process(List chunks) { for (File file : chunks) { tableModel.addResult(new FileItem(file)); + // update found count and status + foundCount++; + statusLabel.setText("Found " + foundCount + " — searching..."); + // If this is the first found file, select it and focus the table + if (foundCount == 1) { + try { + resultsTable.setRowSelectionInterval(0, 0); + resultsTable.requestFocusInWindow(); + } catch (Exception ignore) {} + } } } @@ -153,8 +479,12 @@ public class SearchDialog extends JDialog { protected void done() { searchButton.setEnabled(true); searching = false; + // finalize status + statusProgressBar.setIndeterminate(false); + statusProgressBar.setVisible(false); try { get(); + statusLabel.setText("Done — found " + foundCount + " files"); if (tableModel.getRowCount() == 0) { JOptionPane.showMessageDialog(SearchDialog.this, "Nebyly nalezeny žádné soubory", @@ -162,6 +492,7 @@ public class SearchDialog extends JDialog { JOptionPane.INFORMATION_MESSAGE); } } catch (Exception e) { + statusLabel.setText("Error"); JOptionPane.showMessageDialog(SearchDialog.this, "Chyba při hledání: " + e.getMessage(), "Chyba", @@ -181,6 +512,15 @@ public class SearchDialog extends JDialog { if (selectedRow >= 0) { FileItem item = tableModel.getResult(selectedRow); try { + // Open location inside the application: show parent directory in the focused panel and select the file + java.awt.Window w = SwingUtilities.getWindowAncestor(this); + if (w instanceof MainWindow) { + MainWindow mw = (MainWindow) w; + mw.showFileInFocusedPanel(item.getFile()); + dispose(); + return; + } + // Fallback to opening in system explorer Desktop.getDesktop().open(item.getFile().getParentFile()); } catch (Exception e) { JOptionPane.showMessageDialog(this, @@ -190,6 +530,44 @@ public class SearchDialog extends JDialog { } } } + + /** + * Open the selected file in the internal viewer (read-only) + */ + private void viewSelectedFile() { + int sel = resultsTable.getSelectedRow(); + if (sel >= 0) { + FileItem item = tableModel.getResult(sel); + if (item != null && !item.isDirectory() && !"..".equals(item.getName())) { + try { + Frame owner = (Frame) SwingUtilities.getWindowAncestor(this); + FileEditor viewer = new FileEditor(owner, item.getFile(), config, true); + viewer.setVisible(true); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, "Chyba při otevírání souboru: " + e.getMessage(), "Chyba", JOptionPane.ERROR_MESSAGE); + } + } + } + } + + /** + * Open the selected file in the internal editor (editable) + */ + private void editSelectedFile() { + int sel = resultsTable.getSelectedRow(); + if (sel >= 0) { + FileItem item = tableModel.getResult(sel); + if (item != null && !item.isDirectory() && !"..".equals(item.getName())) { + try { + Frame owner = (Frame) SwingUtilities.getWindowAncestor(this); + FileEditor editor = new FileEditor(owner, item.getFile(), config, false); + editor.setVisible(true); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, "Chyba při otevírání souboru: " + e.getMessage(), "Chyba", JOptionPane.ERROR_MESSAGE); + } + } + } + } /** * Model tabulky pro výsledky hledání