package de.uni_passau.fim.infosun.prophet.util.searchBar; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.swing.*; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreePath; import de.uni_passau.fim.infosun.prophet.plugin.plugins.codeViewerPlugin.CodeViewer; import de.uni_passau.fim.infosun.prophet.plugin.plugins.codeViewerPlugin.fileTree.FileTree; import de.uni_passau.fim.infosun.prophet.plugin.plugins.codeViewerPlugin.fileTree.FileTreeNode; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.SearchContext; import org.fife.ui.rtextarea.SearchEngine; import static de.uni_passau.fim.infosun.prophet.util.language.UIElementNames.getLocalized; /** * A <code>JToolBar</code> containing controls to enable searching for strings or regular expressions globally in every * <code>File</code> displayed by the <code>FileTree</code> of a given <code>CodeViewer</code>. After a search * all nodes in which the search expression was found will be highlighted and made visible in the <code>FileTree</code>. * * @author Robert Futrell * @author Markus Köppen * @author Andreas Hasselberg * @author Georg Seibt */ public class GlobalSearchBar extends JToolBar implements ActionListener { public static final String CAPTION_HIDE = "X"; public static final String CAPTION_FIND = getLocalized("GLOBAL_SEARCH_BAR_SEARCH"); public static final String CAPTION_REGEX = getLocalized("GLOBAL_SEARCH_BAR_REGULAR_EXPRESSION"); public static final String CAPTION_MATCH_CASE = getLocalized("GLOBAL_SEARCH_BAR_CASE_SENSITIVE"); public static final String ACTION_HIDE = "Hide"; public static final String ACTION_FIND = "Global"; private JButton hideButton; private JButton findButton; private JCheckBox regexCB; private JCheckBox matchCaseCB; private JTextField searchField; private CodeViewer codeViewer; private List<SearchBarListener> listeners; private TreeCellRenderer oldRenderer; /** * A <code>DefaultTreeCellRenderer</code> highlights cells if the their nodes are in a given <code>Set</code>. * It will be used with the <code>FileTree</code> that is part of a <code>CodeViewer</code> and therefore * accepts sets of <code>FileTreeNode</code>s. */ private static class Highlighter extends DefaultTreeCellRenderer { private Set<FileTreeNode> highlightedNodes; /** * Constructs a new <code>Highlighter</code> highlighting the nodes in the given set. * * @param highlightedNodes the nodes to highlight */ public Highlighter(Set<FileTreeNode> highlightedNodes) { this.highlightedNodes = highlightedNodes; } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (value instanceof FileTreeNode && highlightedNodes.contains(value)) { setForeground(Color.red); } return this; } } /** * Constructs a new <code>GlobalSearchBar</code> searching through the files displayed by the given * <code>CodeViewer</code>. * * @param codeViewer the <code>CodeViewer</code> whose nodes are to be searched through */ public GlobalSearchBar(CodeViewer codeViewer) { setFloatable(false); this.codeViewer = codeViewer; this.listeners = new ArrayList<>(); Dimension sepDim = new Dimension(5, 0); hideButton = new JButton(CAPTION_HIDE); hideButton.setActionCommand(ACTION_HIDE); hideButton.addActionListener(this); add(hideButton); findButton = new JButton(CAPTION_FIND); findButton.setActionCommand(ACTION_FIND); findButton.addActionListener(this); add(findButton); addSeparator(sepDim); searchField = new JTextField(30); searchField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { if (event.getKeyCode() == KeyEvent.VK_ENTER) { findButton.doClick(); } } }); add(searchField); addSeparator(sepDim); regexCB = new JCheckBox(CAPTION_REGEX); add(regexCB); addSeparator(sepDim); matchCaseCB = new JCheckBox(CAPTION_MATCH_CASE); add(matchCaseCB); addSeparator(sepDim); } /** * Adds a <code>SearchBarListener</code> to this <code>GlobalSearchBar</code>. * * @param listener * the <code>SearchBarListener</code> to add */ public void addSearchBarListener(SearchBarListener listener) { listeners.add(listener); } /** * Removes a <code>SearchBarListener</code> from this <code>GlobalSearchBar</code>. * * @param listener * the <code>SearchBarListener</code> to remove */ public void removeSearchBarListener(SearchBarListener listener) { listeners.remove(listener); } @Override public void grabFocus() { searchField.grabFocus(); } @Override public void actionPerformed(ActionEvent action) { String command = action.getActionCommand(); FileTree fileTree = codeViewer.getFileTree(); if (command.equals(ACTION_HIDE)) { if (oldRenderer != null) { fileTree.setCellRenderer(oldRenderer); fileTree.repaint(); oldRenderer = null; } setVisible(false); return; } String text = searchField.getText(); if (text.isEmpty()) { return; } SearchContext searchContext = new SearchContext(); searchContext.setSearchFor(text); searchContext.setSearchForward(true); searchContext.setMatchCase(matchCaseCB.isSelected()); searchContext.setWholeWord(false); searchContext.setRegularExpression(regexCB.isSelected()); FileTreeNode treeRoot = fileTree.getModel().getRoot(); Stream<FileTreeNode> fileNodes = treeRoot.preOrder().stream().filter(FileTreeNode::isFile); Predicate<FileTreeNode> textFilter = node -> contains(node, searchContext); Set<FileTreeNode> foundNodes = fileNodes.filter(textFilter).collect(Collectors.toSet()); if (!foundNodes.isEmpty()) { if (oldRenderer == null) { oldRenderer = fileTree.getCellRenderer(); } fileTree.setCellRenderer(new Highlighter(foundNodes)); fileTree.repaint(); FileTreeNode[] path; for (FileTreeNode node : foundNodes) { path = fileTree.getModel().buildPath(node.getFile()); fileTree.makeVisible(new TreePath(path)); } } for (SearchBarListener listener : listeners) { listener.searched(command, text, !foundNodes.isEmpty()); } } /** * Checks whether using the given <code>context</code> on the text of the file represented by <code>node</code> * yields a result. * * @param node the node whose text content is to be searched through * @param context the <code>SearchContext</code> to pass to the <code>SearchContext</code> * @return <code>true</code> iff <code>SearchEngine</code> finds a result in the <code>FileTreeNode</code>s text */ private boolean contains(FileTreeNode node, SearchContext context) { if (!node.isFile()) { return false; } JTextArea textArea; try { textArea = new RSyntaxTextArea(new String(Files.readAllBytes(node.getFile().toPath()), StandardCharsets.UTF_8)); textArea.setCaretPosition(0); } catch (IOException e) { System.err.println("Could not search through the contents of " + node.getFile().getName()); return false; } return SearchEngine.find(textArea, context).wasFound(); } /** * Returns the <code>JCheckBox</code> determining whether to use regular expressions when searching. * * @return the <code>JCheckBox</code> */ public JCheckBox getRegexCB() { return regexCB; } }