From 26d62d29021589b4f4fe5ba84784d93eff0443a7 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Tue, 20 Jan 2026 09:50:16 +0100 Subject: [PATCH] search in file --- .../cz/kamma/kfmanager/ui/FileEditor.java | 210 +++++++++++++++++- 1 file changed, 205 insertions(+), 5 deletions(-) diff --git a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java index 9a0813c..4a11da1 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java @@ -35,10 +35,17 @@ public class FileEditor extends JDialog { // Allow loading entire file into memory up to this limit (100 MB) private final long maxFullLoadBytes = 100L * 1024L * 1024L; // 100 MB private JPanel hexControlPanel = null; + private JPanel northPanel = null; private JButton prevPageBtn = null; private JButton nextPageBtn = null; private JLabel pageOffsetLabel = null; private javax.swing.undo.UndoManager undoManager; + + // Search support + private JPanel searchPanel; + private JTextField searchField; + private JLabel searchStatusLabel; + private static String lastSearchValue = ""; public FileEditor(Window parent, File file, AppConfig config, boolean readOnly) { super(parent, (readOnly ? "Prohlížeč - " : "Editor - ") + file.getName(), ModalityType.MODELESS); @@ -61,9 +68,164 @@ public class FileEditor extends JDialog { }); } + private void initSearchPanel() { + searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2)); + searchPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.GRAY)); + + searchField = new JTextField(20); + searchField.addActionListener(e -> findNext()); + + JButton nextBtn = new JButton("Dále"); + nextBtn.addActionListener(e -> findNext()); + + JButton prevBtn = new JButton("Zpět"); + prevBtn.addActionListener(e -> findPrevious()); + + JButton closeBtn = new JButton("X"); + closeBtn.setMargin(new Insets(0, 5, 0, 5)); + closeBtn.addActionListener(e -> hideSearchPanel()); + + searchStatusLabel = new JLabel(""); + + searchPanel.add(new JLabel("Hledat:")); + searchPanel.add(searchField); + searchPanel.add(nextBtn); + searchPanel.add(prevBtn); + searchPanel.add(closeBtn); + searchPanel.add(searchStatusLabel); + + searchPanel.setVisible(false); + } + + private void showSearchPanel() { + searchPanel.setVisible(true); + String selection = textArea.getSelectedText(); + if (selection != null && !selection.isEmpty() && !selection.contains("\n")) { + searchField.setText(selection); + lastSearchValue = selection; + } else if (!lastSearchValue.isEmpty()) { + searchField.setText(lastSearchValue); + } else if (config != null) { + java.util.List hist = config.getContentSearchHistory(); + if (hist != null && !hist.isEmpty()) { + lastSearchValue = hist.get(0); + searchField.setText(lastSearchValue); + } + } + + searchField.requestFocusInWindow(); + searchField.selectAll(); + + SwingUtilities.invokeLater(() -> { + searchField.requestFocusInWindow(); + searchField.selectAll(); + }); + + if (northPanel != null) { + northPanel.revalidate(); + northPanel.repaint(); + } + revalidate(); + } + + private void hideSearchPanel() { + searchPanel.setVisible(false); + textArea.requestFocusInWindow(); + revalidate(); + } + + private void findNext() { + String text = searchField.getText(); + if (text.isEmpty()) text = lastSearchValue; + if (text == null || text.isEmpty()) return; + lastSearchValue = text; + updateSearchHistory(text); + + if (searchField.getText().isEmpty()) searchField.setText(text); + + String content = textArea.getText(); + int start = textArea.getCaretPosition(); + + // If we have a selection that matches, start after it + if (textArea.getSelectionEnd() > textArea.getSelectionStart()) { + if (content.substring(textArea.getSelectionStart(), textArea.getSelectionEnd()).equalsIgnoreCase(text)) { + start = textArea.getSelectionEnd(); + } + } + + int idx = content.toLowerCase().indexOf(text.toLowerCase(), start); + if (idx == -1) { + // wrap around + idx = content.toLowerCase().indexOf(text.toLowerCase(), 0); + } + + if (idx != -1) { + textArea.setSelectionStart(idx); + textArea.setSelectionEnd(idx + text.length()); + textArea.getCaret().setSelectionVisible(true); + try { + Rectangle rect = textArea.modelToView2D(idx).getBounds(); + textArea.scrollRectToVisible(rect); + } catch (Exception ignore) {} + searchStatusLabel.setText(""); + } else { + searchStatusLabel.setText("Nenalezeno"); + } + } + + private void findPrevious() { + String text = searchField.getText(); + if (text.isEmpty()) text = lastSearchValue; + if (text == null || text.isEmpty()) return; + lastSearchValue = text; + updateSearchHistory(text); + + if (searchField.getText().isEmpty()) searchField.setText(text); + + String content = textArea.getText(); + int start = textArea.getSelectionStart() - 1; + if (start < 0) start = content.length() - 1; + + int idx = content.toLowerCase().lastIndexOf(text.toLowerCase(), start); + if (idx == -1) { + // wrap around + idx = content.toLowerCase().lastIndexOf(text.toLowerCase(), content.length() - 1); + } + + if (idx != -1) { + textArea.setSelectionStart(idx); + textArea.setSelectionEnd(idx + text.length()); + textArea.getCaret().setSelectionVisible(true); + try { + Rectangle rect = textArea.modelToView2D(idx).getBounds(); + textArea.scrollRectToVisible(rect); + } catch (Exception ignore) {} + searchStatusLabel.setText(""); + } else { + searchStatusLabel.setText("Nenalezeno"); + } + } + + private void updateSearchHistory(String text) { + if (config == null || text == null || text.isEmpty()) return; + java.util.List hist = new java.util.ArrayList<>(config.getContentSearchHistory()); + hist.remove(text); + hist.add(0, text); + if (hist.size() > 20) hist = hist.subList(0, 20); + config.saveContentSearchHistory(hist); + config.saveConfig(); + } + private void initComponents() { setLayout(new BorderLayout()); + northPanel = new JPanel(); + northPanel.setLayout(new BoxLayout(northPanel, BoxLayout.Y_AXIS)); + add(northPanel, BorderLayout.NORTH); + + initSearchPanel(); + northPanel.add(searchPanel); + // Menu bar createMenuBar(); @@ -284,6 +446,18 @@ public class FileEditor extends JDialog { selectAllItem.addActionListener(e -> textArea.selectAll()); editMenu.add(selectAllItem); + editMenu.addSeparator(); + + JMenuItem findItem = new JMenuItem("Hledat..."); + findItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK)); + findItem.addActionListener(e -> showSearchPanel()); + editMenu.add(findItem); + + JMenuItem findNextItem = new JMenuItem("Hledat další"); + findNextItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0)); + findNextItem.addActionListener(e -> findNext()); + editMenu.add(findNextItem); + // Update menu items state when menu is selected editMenu.addMenuListener(new javax.swing.event.MenuListener() { @Override @@ -352,8 +526,14 @@ public class FileEditor extends JDialog { JComponent.WHEN_IN_FOCUSED_WINDOW); } - // ESC - Zavřít - rootPane.registerKeyboardAction(e -> closeEditor(), + // ESC - Zavřít nebo skrýt hledání + rootPane.registerKeyboardAction(e -> { + if (searchPanel.isVisible()) { + hideSearchPanel(); + } else { + closeEditor(); + } + }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); @@ -394,6 +574,19 @@ public class FileEditor extends JDialog { rootPane.registerKeyboardAction(e -> { if (undoManager.canRedo()) undoManager.redo(); }, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); + + // Hledání + rootPane.registerKeyboardAction(e -> showSearchPanel(), + KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + rootPane.registerKeyboardAction(e -> findNext(), + KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + + rootPane.registerKeyboardAction(e -> findPrevious(), + KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.SHIFT_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); } private void setHexMode(boolean on) { @@ -422,9 +615,10 @@ public class FileEditor extends JDialog { textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize())); ensureHexControls(); if (hexControlPanel.getParent() == null) { - add(hexControlPanel, BorderLayout.NORTH); + northPanel.add(hexControlPanel); } hexControlPanel.setVisible(true); + northPanel.revalidate(); } else { // switch back to text view if possible // close RA if open @@ -434,6 +628,10 @@ public class FileEditor extends JDialog { loadFile(); textArea.setEditable(!readOnly); textArea.setFont(config.getEditorFont()); + if (hexControlPanel != null) { + hexControlPanel.setVisible(false); + northPanel.revalidate(); + } } applyAppearance(); updateStatus(); @@ -593,8 +791,9 @@ public class FileEditor extends JDialog { pageOffsetBytes = 0L; loadHexPage(); ensureHexControls(); - if (hexControlPanel.getParent() == null) add(hexControlPanel, BorderLayout.NORTH); + if (hexControlPanel.getParent() == null) northPanel.add(hexControlPanel); hexControlPanel.setVisible(true); + northPanel.revalidate(); textArea.setEditable(false); textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize())); hexMode = true; @@ -608,8 +807,9 @@ public class FileEditor extends JDialog { textArea.setEditable(false); textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize())); ensureHexControls(); - if (hexControlPanel.getParent() == null) add(hexControlPanel, BorderLayout.NORTH); + if (hexControlPanel.getParent() == null) northPanel.add(hexControlPanel); hexControlPanel.setVisible(true); + northPanel.revalidate(); } else if (hexMode) { buildHexViewText(0L); } else {