/*
* Copyright (c) 1997-2024 IDRsolutions (https://www.idrsolutions.com)
*/
package org.jpedal.examples.viewer.gui.swing;
import org.jpedal.PdfDecoderInt;
import org.jpedal.display.Display;
import org.jpedal.display.GUIDisplay;
import org.jpedal.examples.viewer.Commands;
import org.jpedal.examples.viewer.Values;
import org.jpedal.examples.viewer.commands.NextResults;
import org.jpedal.examples.viewer.commands.PageNavigator;
import org.jpedal.examples.viewer.commands.Scroll;
import org.jpedal.examples.viewer.gui.SwingGUI;
import org.jpedal.external.JPedalActionHandler;
import org.jpedal.external.Options;
import org.jpedal.grouping.DefaultSearchListener;
import org.jpedal.grouping.PdfGroupingAlgorithms;
import org.jpedal.grouping.SearchType;
import org.jpedal.objects.PdfPageData;
import org.jpedal.utils.LogWriter;
import org.jpedal.utils.Messages;
import org.jpedal.utils.repositories.generic.Vector_Rectangle_Int;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.JTextComponent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.ComponentListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* Provides interactive search Window and search capabilities.
* <p>
* This class extends JFrame to allow the option to display the search functionality
* as an external window to the viewer. There are other ways to display the search
* interface using the method setViewStyle(int style);
* <p>
* For instance the search interface can be displayed as follows.
* setViewStyle(SwingSearchWindow.SEARCH_EXTERNAL_WINDOW); // Show search interface as external window
* setViewStyle(SwingSearchWindow.SEARCH_TABBED_PANE); // Show search interface on side tab bar
* setViewStyle(SwingSearchWindow.SEARCH_MENU_BAR); // Show search interface on button tool bar
*/
public class SwingSearchWindow extends JFrame {
public static final int SEARCH_EXTERNAL_WINDOW = 0;
public static final int SEARCH_TABBED_PANE = 1;
public static final int SEARCH_MENU_BAR = 2;
//Flag to show search has happened and needs reset
private boolean hasSearched;
private Map<Integer, int[][]> searchAreas;
private boolean backGroundSearch;
private int searchKey;
private final ExecutorService service = Executors.newCachedThreadPool();
private Future<?> searchFuture;
private int style;
private boolean isSetup; //flag to stop multiple listeners
private boolean usingMenuBarSearch;
private int lastPage = -1;
private String defaultMessage = "Search PDF Here";
private JTextField searchText;
private JTextField searchCount;
private DefaultListModel<String> listModel;
private SearchList resultsList;
private JLabel label;
private JPanel advancedPanel;
private JComboBox<String> searchType;
private JCheckBox wholeWordsOnlyBox, caseSensitiveBox, multiLineBox, highlightAll, searchAll, useRegEx, searchHighlightedOnly, ignoreWhiteSpace;
private boolean updateListDuringSearch = true;
private JButton searchButton;
private int itemFoundCount;
private String[] searchTerms = {""};
private String lastSearchTerms;
private boolean singlePageSearch; //Search this page only
private Values commonValues;
private PdfDecoderInt decode_pdf;
private int searchTypeParameters;
private int firstPageWithResults;
private boolean deleteOnClick; //deletes message when user starts typing
private final Map<Integer, Integer> textPages = new HashMap<>(); //used when finding text to highlight on page
private final Map<Integer, Object> textRectangles = new HashMap<>();
private final JPanel nav = new JPanel();
private final SwingGUI currentGUI;
private final Runnable searchRunner = new Runnable() {
@Override
public void run() {
itemFoundCount = 0;
resultsList.setSelectedIndex(-1); //Ensure nothing selected so first result is highlighted correctly.
Thread.currentThread().setName("SwingSearchWindow");
clearCurrentResults();
final long startTime = System.currentTimeMillis();
final boolean searchingInBackground = backGroundSearch;
//Now local variable is set we can turn off global variable
backGroundSearch = false;
final int currentKey = searchKey;
try {
// [AWI]: Create a new list model to append the search results to
// NOTE: This was added to prevent a deadlock issue that occurred on the
// EDT when the search resulted in a large number of hits
final DefaultListModel<String> resultListModel;
if (updateListDuringSearch) {
resultListModel = listModel;
} else {
resultListModel = new DefaultListModel<>();
}
int start = 1;
int end = decode_pdf.getPageCount() + 1;
if (singlePageSearch) {
start = decode_pdf.getPageNumber();
end = start + 1;
}
if (!searchPages(start, end, currentKey, resultListModel, searchingInBackground)) {
setResultsInGUI(resultListModel, searchingInBackground, startTime);
}
} catch (final Exception e) {
LogWriter.writeLog("Exception in handling search " + e);
if (currentGUI != null && currentGUI.getButtons() != null && !searchingInBackground) {
if (currentGUI.getButtons().getButton(Commands.NEXTRESULT) != null) {
currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(false);
}
if (currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT) != null) {
currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(false);
}
}
} finally {
commonValues.setIsSearching(false);
}
}
private void setResultsInGUI(final DefaultListModel<String> resultListModel, final boolean searchingInBackground, final long searchStartTime) throws InvocationTargetException, InterruptedException {
if (!searchingInBackground) {
searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound") + ' ' + itemFoundCount + " "
+ Messages.getMessage("PdfViewerSearch.Done"));
}
// [AWI]: Update the list model displayed in the results list
// NOTE: This was added here to address an EDT lock-up and contention issue
// that can occur when a large result set is returned from a search. By
// setting the model once at the end, we only take the hit for updating the
// JList once.
final int listSize = resultListModel.size();
if (listSize > 1500) {
resultListModel.removeRange(1500, (listSize - 1));
resultListModel.addElement("Too many results to list...");
}
listModel = resultListModel;
SwingUtilities.invokeAndWait(() -> {
if (resultsList != null && listModel != null) {
resultsList.setModel(listModel);
}
});
currentGUI.setResults(resultsList);
resultsList.setSelectedIndex(0);
decode_pdf.repaint();
//switch on buttons as soon as search produces valid results
// [AWI] not used
if (!searchingInBackground) {
currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(true);
currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(true);
}
if (LogWriter.isRunningFromIDE) {
//show time and memory usage
System.out.println("Search memory=" + ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1_000) + 'K');
System.out.println("Search time=" + (((float) Math.abs(((System.currentTimeMillis() - searchStartTime) / 100))) / 10) + 's');
}
}
private boolean searchPages(final int start, final int end, final int currentKey,
final DefaultListModel<String> resultListModel, final boolean searchingInBackground) throws Exception {
final int currentPage = commonValues.getCurrentPage();
for (int i = start; i != end; i++) {
final int page;
if (usingMenuBarSearch) {
//When using menu bar, break from loop if result found
if (resultsList.getResultCount() >= 1) {
break;
}
if (currentPage + (i - 1) > commonValues.getPageCount()) {
page = currentPage + (i - 1) - commonValues.getPageCount();
} else {
page = currentPage + (i - 1);
}
} else {
page = i;
}
if (!Thread.currentThread().isInterrupted()) {
if (searchAreas != null) {
final int[][] highlights = searchAreas.get(page);
if (highlights != null) {
for (final int[] a : highlights) {
//[AWI]: Update the search method to take the target list model as a parameter
searchPage(page, a[0], a[1], a[0] + a[2], a[1] + a[3], currentKey, resultListModel);
}
}
} else {
//[AWI]: Update the search method to take the target list model as a parameter
searchPage(page, currentKey, resultListModel);
}
} else {
return true;
}
// new value or 16 pages elapsed
if (!searchingInBackground && (resultListModel.getSize() > 0) || ((page % 16) == 0)) {
searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound") + ' ' + itemFoundCount + ' '
+ Messages.getMessage("PdfViewerSearch.Scanning") + page);
}
}
return false;
}
};
/**
* A search interface for the viewer complete with elements to enable and disable
* the various search options that are available.
*
* @param currentGUI SwingGUI object used for the Viewers user interface
*/
public SwingSearchWindow(final SwingGUI currentGUI) {
this.currentGUI = currentGUI;
setName("searchFrame");
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
}
/**
* Set if search should use the search whole words only rule
*
* @param wholeWords true to enable whole words only rule, false to disable
*/
public void setWholeWords(final boolean wholeWords) {
wholeWordsOnlyBox.setSelected(wholeWords);
}
/**
* Set if search should use the case sensitive search rule
*
* @param caseSensitive true to enable case sensitive search rule, false to disable
*/
public void setCaseSensitive(final boolean caseSensitive) {
caseSensitiveBox.setSelected(caseSensitive);
}
/**
* Set if the search should use the find multi line results rule
*
* @param multiLine true to enable find multi line results rule, false to disable
*/
public void setMultiLine(final boolean multiLine) {
multiLineBox.setSelected(multiLine);
}
/**
* Set if the search should ignore space characters in page content and search terms
*
* @param ignoreSpaces true to enable ignore space character results rule, false to disable
*/
public void setIgnoreSpaces(final boolean ignoreSpaces) {
ignoreWhiteSpace.setSelected(ignoreSpaces);
}
/**
* Set if the search result list should be updated as the search continues
*
* @param updateListDuringSearch true to enable live list updating, false to disable
*/
public void setUpdateListDuringSearch(final boolean updateListDuringSearch) {
this.updateListDuringSearch = updateListDuringSearch;
}
/**
* Not part of API - used internally
* <p>
* Used to initialise the search interface ready for use.
*
* @param dec PdfDecoderInt object used for the current PDF
* @param values Values object used by the user interface
*/
@SuppressWarnings({"OverlyLongMethod", "java:S138"})
public void init(final PdfDecoderInt dec, final Values values) {
decode_pdf = dec;
commonValues = values;
if (isSetup) { //global variable so do NOT reinitialise
searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound") + ' ' + itemFoundCount);
searchText.selectAll();
searchText.grabFocus();
} else {
isSetup = true;
setTitle(Messages.getMessage("PdfViewerSearchGUITitle.DefaultMessage"));
defaultMessage = Messages.getMessage("PdfViewerSearchGUI.DefaultMessage");
searchText = new JTextField(10);
searchText.setText(defaultMessage);
searchText.setName("searchText");
createAdvancedOptionsPanel();
nav.setLayout(new BorderLayout());
addWindowListener(new WindowAdapter() {
//flush objects on close
@Override
public void windowClosing(final WindowEvent arg0) {
removeSearchWindow(true);
}
});
nav.add(searchButton, BorderLayout.EAST);
nav.add(searchText, BorderLayout.CENTER);
searchAll = new JCheckBox();
searchAll.setSelected(true);
searchAll.setText(Messages.getMessage("PdfViewerSearch.CheckBox"));
final JPanel topPanel = new JPanel();
topPanel.setLayout(new BorderLayout());
topPanel.add(searchAll, BorderLayout.NORTH);
label = new JLabel("<html><center> " + "Show Advanced");
final Color bgColor = label.getBackground();
if (bgColor != null) {
final double luma = ((0.299 * bgColor.getRed()) + (0.587 * bgColor.getGreen()) + (0.114 * bgColor.getBlue())) / 255;
if (luma > 0.5) {
label.setForeground(Color.BLUE);
} else {
label.setForeground(new Color(64, 128, 255));
}
} else {
label.setForeground(Color.BLUE);
}
label.setName("advSearch");
label.addMouseListener(new MouseAdapter() {
boolean isVisible;
String text = "Show Advanced";
@Override
public void mouseEntered(final MouseEvent e) {
if (GUIDisplay.allowChangeCursor) {
nav.setCursor(new Cursor(Cursor.HAND_CURSOR));
}
label.setText("<html><center><a href=" + text + '>' + text + "</a></center>");
}
@Override
public void mouseExited(final MouseEvent e) {
if (GUIDisplay.allowChangeCursor) {
nav.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
label.setText("<html><center>" + text);
}
@Override
public void mouseClicked(final MouseEvent e) {
if (isVisible) {
text = Messages.getMessage("PdfViewerSearch.ShowOptions");
label.setText("<html><center><a href=" + text + '>' + text + "</a></center>");
advancedPanel.setVisible(false);
} else {
text = Messages.getMessage("PdfViewerSearch.HideOptions");
label.setText("<html><center><a href=" + text + '>' + text + "</a></center>");
advancedPanel.setVisible(true);
}
isVisible = !isVisible;
}
});
label.setBorder(BorderFactory.createEmptyBorder(3, 4, 4, 4));
topPanel.add(label, BorderLayout.SOUTH);
nav.add(topPanel, BorderLayout.NORTH);
itemFoundCount = 0;
textPages.clear();
textRectangles.clear();
listModel = null;
searchCount = new JTextField(Messages.getMessage("PdfViewerSearch.ItemsFound") + ' ' + itemFoundCount);
searchCount.setEditable(false);
nav.add(searchCount, BorderLayout.SOUTH);
listModel = new DefaultListModel<>();
resultsList = new SearchList(listModel, textPages, textRectangles);
resultsList.setName("results");
addListeners();
addComponentsToInterface();
}
}
private void createAdvancedOptionsPanel() {
/*
* [AWI] Add a focus listener to detect when keyboard input is needed in the search text field. This
* registration was added to support systems configured with touchscreens and virtual keyboards.
*/
searchText.addFocusListener(new SearchTextFocusListener());
searchButton = new JButton(Messages.getMessage("PdfViewerSearch.Button"));
advancedPanel = new JPanel(new GridBagLayout());
searchType = new JComboBox<>(new String[]{Messages.getMessage("PdfViewerSearch.MatchWhole"),
Messages.getMessage("PdfViewerSearch.MatchAny")});
wholeWordsOnlyBox = new JCheckBox(Messages.getMessage("PdfViewerSearch.WholeWords"));
wholeWordsOnlyBox.setName("wholeWords");
searchHighlightedOnly = new JCheckBox(Messages.getMessage("PdfViewerSearch.HighlightsOnly"));
searchHighlightedOnly.setName("highlightsOnly");
caseSensitiveBox = new JCheckBox(Messages.getMessage("PdfViewerSearch.CaseSense"));
caseSensitiveBox.setName("caseSensitive");
multiLineBox = new JCheckBox(Messages.getMessage("PdfViewerSearch.MultiLine"));
multiLineBox.setName("multiLine");
highlightAll = new JCheckBox(Messages.getMessage("PdfViewerSearch.HighlightsCheckBox"));
highlightAll.setName("highlightAll");
useRegEx = new JCheckBox(Messages.getMessage("PdfViewerSearch.RegExCheckBox"));
useRegEx.setName("useregex");
ignoreWhiteSpace = new JCheckBox(Messages.getMessage("PdfViewerSearch.IgnoreWhiteSpace"));
ignoreWhiteSpace.setName("ignoreWhiteSpace");
searchType.setName("combo");
final GridBagConstraints c = new GridBagConstraints();
advancedPanel.setPreferredSize(new Dimension(advancedPanel.getPreferredSize().width, 150));
c.gridx = 0;
c.gridy = 0;
c.anchor = GridBagConstraints.PAGE_START;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.weighty = 0;
advancedPanel.add(new JLabel(Messages.getMessage("PdfViewerSearch.ReturnResultsAs")), c);
c.insets = new Insets(5, 0, 0, 0);
c.gridy = 1;
advancedPanel.add(searchType, c);
c.gridy = 2;
advancedPanel.add(new JLabel(Messages.getMessage("PdfViewerSearch.AdditionalOptions")), c);
c.insets = new Insets(0, 0, 0, 0);
c.weighty = 1;
c.gridy = 3;
advancedPanel.add(wholeWordsOnlyBox, c);
c.weighty = 1;
c.gridy = 4;
advancedPanel.add(caseSensitiveBox, c);
c.weighty = 1;
c.gridy = 5;
advancedPanel.add(multiLineBox, c);
c.weighty = 1;
c.gridy = 6;
advancedPanel.add(highlightAll, c);
c.weighty = 1;
c.gridy = 7;
advancedPanel.add(useRegEx, c);
c.weighty = 1;
c.gridy = 8;
advancedPanel.add(searchHighlightedOnly, c);
c.weighty = 1;
c.gridy = 9;
advancedPanel.add(ignoreWhiteSpace, c);
advancedPanel.setVisible(false);
}
@SuppressWarnings({"OverlyLongMethod", "java:S138"})
private void addListeners() {
//Listener to highlight text on item selected
resultsList.addListSelectionListener(new searchListListener());
resultsList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
//setup searching
searchButton.addActionListener(e -> {
if (!commonValues.isSearching()) {
try {
searchTypeParameters = getSearchOptionsAsInt();
final String textToFind = searchText.getText().trim();
if (searchType.getSelectedIndex() == 0) { // find exact word or phrase
searchTerms = new String[]{textToFind};
} else { // match any of the words
searchTerms = textToFind.split(" ");
for (int i = 0; i < searchTerms.length; i++) {
searchTerms[i] = searchTerms[i].trim();
}
}
singlePageSearch = !searchAll.isSelected();
searchText();
} catch (final Exception e1) {
LogWriter.writeLog(e1);
}
} else {
commonValues.setIsSearching(false);
searchButton.setText(Messages.getMessage("PdfViewerSearch.Button"));
}
decode_pdf.requestFocus();
});
searchText.selectAll();
deleteOnClick = true;
final SwingSearchWindow finalSearchWindow = this;
searchText.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(final KeyEvent e) {
final String textToFind = searchText.getText().trim();
if (textToFind.isEmpty()) {
lastSearchTerms = null;
}
final int id = e.getID();
if (id == KeyEvent.KEY_TYPED) {
final char key = e.getKeyChar();
if (key == '\n') {
if (!decode_pdf.isOpen()) {
currentGUI.showMessageDialog(Messages.getMessage("PdfViewerFileMenuFind.noFile"));
} else {
try {
commonValues.setIsSearching(false);
searchTypeParameters = getSearchOptionsAsInt();
if (textToFind.equals(lastSearchTerms)) {
NextResults.execute(null, commonValues, finalSearchWindow, currentGUI, decode_pdf);
} else {
lastSearchTerms = textToFind;
if (searchType.getSelectedIndex() == 0) { // find exact word or phrase
searchTerms = new String[]{textToFind};
} else { // match any of the words
searchTerms = textToFind.split(" ");
for (int i = 0; i < searchTerms.length; i++) {
searchTerms[i] = searchTerms[i].trim();
}
}
singlePageSearch = !searchAll.isSelected();
searchText();
if (!usingMenuBarSearch) {
decode_pdf.requestFocus();
}
}
} catch (final Exception e1) {
LogWriter.writeLog("Error performing search " + e1.getMessage());
}
}
}
}
}
@Override
public void keyReleased(final KeyEvent arg0) {
if (currentGUI.getButtons().getButton(Commands.NEXTRESULT) != null) {
currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(false);
}
if (currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT) != null) {
currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(false);
}
}
});
searchText.addFocusListener(new FocusListener() {
@Override
public void focusLost(final FocusEvent e) {
if (((JTextComponent) e.getComponent()).getText().isEmpty()) {
((JTextComponent) e.getComponent()).setText(defaultMessage);
deleteOnClick = true;
}
}
@Override
public void focusGained(final FocusEvent e) {
//clear when user types
if (deleteOnClick) {
deleteOnClick = false;
((JTextComponent) e.getComponent()).setText("");
}
}
});
}
private void addComponentsToInterface() {
if (style == SEARCH_EXTERNAL_WINDOW || style == SEARCH_TABBED_PANE) {
//build frame
final JScrollPane scrollPane = new JScrollPane();
scrollPane.getViewport().add(resultsList);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.getVerticalScrollBar().setUnitIncrement(80);
scrollPane.getHorizontalScrollBar().setUnitIncrement(80);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(scrollPane, BorderLayout.CENTER);
getContentPane().add(nav, BorderLayout.NORTH);
getContentPane().add(advancedPanel, BorderLayout.SOUTH);
//position and size
final Container frame = currentGUI.getFrame();
if (style == SEARCH_EXTERNAL_WINDOW) {
final int w = 230;
final int h = frame.getHeight();
final int x1 = frame.getLocationOnScreen().x;
int x = frame.getWidth() + x1;
final int y = frame.getLocationOnScreen().y;
final Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
final int width = d.width;
if (x + w > width) {
x = width - w;
frame.setSize(x - x1, frame.getHeight());
}
setSize(w, h);
setLocation(x, y);
}
searchAll.setFocusable(false);
searchText.grabFocus();
if (style == SEARCH_TABBED_PANE) {
currentGUI.setSearchText(searchText);
}
} else {
//Whole Panel not used, take what is needed
currentGUI.setSearchText(searchText);
}
}
/**
* Find text in PDF without using the search window. This will not initialise the
* user interface when performing searches.
*
* @param dec PdfDecoderInt object used for the PDF to be searched
* @param values Values object used by the user interface
* @param searchType int value using bit flags to control search options
* @param listOfTerms true to treat search value as a space separated list, false to treat as a single search term
* @param singlePageOnly true to search just the current page, false to search the entire document
* @param searchValue String value to be searched, this can be either a single search term or a space separated list of terms controlled by listOfTerms
*/
public void findWithoutWindow(final PdfDecoderInt dec, final Values values, final int searchType, final boolean listOfTerms, final boolean singlePageOnly, final String searchValue) {
init(dec, values);
if (!commonValues.isSearching()) {
backGroundSearch = true;
commonValues.setIsSearching(true);
decode_pdf = dec;
commonValues = values;
if (!listOfTerms) { // find exact word or phrase
searchTerms = new String[]{searchValue};
} else { // match any of the words
searchTerms = searchValue.split(" ");
for (int i = 0; i < searchTerms.length; i++) {
searchTerms[i] = searchTerms[i].trim();
}
}
searchTypeParameters = searchType;
singlePageSearch = singlePageOnly;
find(dec, values);
} else {
currentGUI.showMessageDialog("Please wait for search to finish before starting another.");
}
}
/**
* Find text in PDF using the search window. If the search window has not been
* initialised it will be initialised here.
*
* @param dec PdfDecoderInt object used for the PDF to be searched
* @param values Values object used by the user interface
*/
public void find(final PdfDecoderInt dec, final Values values) {
//pop up new window to search text (initialise if required
if (!backGroundSearch) {
init(dec, values);
if (style == SEARCH_EXTERNAL_WINDOW) {
setVisible(true);
}
} else {
try {
searchText();
} catch (final Exception e) {
LogWriter.writeLog(e);
}
}
}
/**
* Remove the search window from view and clear the results if required.
*
* @param justHide true to keep the results, false to clear results
*/
public void removeSearchWindow(final boolean justHide) {
setVisible(false);
if (isSetup && !justHide) {
if (listModel != null) {
listModel.clear();
}
itemFoundCount = 0;
commonValues.setIsSearching(false);
}
//lose any highlights and force redraw with non-existent box
if (decode_pdf != null) {
decode_pdf.getTextLines().clearHighlights();
decode_pdf.repaint();
}
}
/**
* Clears the current set of results from the result list
*/
private void clearCurrentResults() {
listModel.clear();
resultsList.clearSelection();
textPages.clear();
textRectangles.clear();
itemFoundCount = 0;
decode_pdf.getTextLines().clearHighlights();
//Refresh display after clearing highlights
decode_pdf.repaintPane(0);
decode_pdf.getPages().refreshDisplay();
resultsList.repaint();
}
private void searchText() {
//Flag is we are using menu bar search
usingMenuBarSearch = style == SEARCH_MENU_BAR;
//Alter searchKey so the update thread knows not to update
searchKey++;
//Reset last page searched flag.
lastPage = -1;
/*
* To prevent the chance of hitting the maximum value of searchKey
* we should reset long after a value large enough to guarantee
* any thread using a searchKey of 0 is closed.
*/
if (searchKey > 3_000) {
searchKey = 0;
}
//Cancel a search if currently exists
if (searchFuture != null) {
searchFuture.cancel(true);
}
if (!usingMenuBarSearch && (searchTypeParameters & SearchType.SEARCH_HIGHLIGHTS_ONLY) == SearchType.SEARCH_HIGHLIGHTS_ONLY) {
searchAreas = decode_pdf.getTextLines().getAllHighlights();
} else {
searchAreas = null;
}
searchFuture = service.submit(searchRunner);
}
/**
* Performs the currently set up search on the given page
*
* @param page :: Page to be searched with the currently set term and settings
* @param currentKey :: The current search key, used to control results update when search ends
* @param model :: [AWI] The list model to append the search results to
*/
private void searchPage(final int page, final int currentKey, final DefaultListModel<String> model) throws Exception {
final PdfPageData currentPageData = decode_pdf.getPdfPageData();
final int x1 = currentPageData.getMediaBoxX(page);
final int x2 = currentPageData.getMediaBoxWidth(page) + x1;
final int y2 = currentPageData.getMediaBoxY(page);
final int y1 = currentPageData.getMediaBoxHeight(page) + y2;
searchPage(page, x1, y1, x2, y2, currentKey, model);
}
/**
* Performs the currently set up search on the given page
*
* @param x1 the left x cord
* @param y1 the upper y cord
* @param x2 the right x cord
* @param y2 the lower y cord
* @param page :: Page to be searched with the currently set term and settings
* @param currentKey :: The current search key, used to control results update when search ends
* @param model :: [AWI] The list model to append the search results to
*/
private void searchPage(final int page, final int x1, final int y1, final int x2, final int y2, final int currentKey, final DefaultListModel<String> model) throws Exception {
firstPageWithResults = 0;
final PdfGroupingAlgorithms grouping;
final PdfPageData pageSize = decode_pdf.getPdfPageData();
if (decode_pdf.getDisplayView() == Display.SINGLE_PAGE && page == commonValues.getCurrentPage()) {
grouping = decode_pdf.getGroupingObject();
} else {
decode_pdf.decodePageInBackground(page);
grouping = decode_pdf.getBackgroundGroupingObject();
}
final DefaultSearchListener listener = new DefaultSearchListener();
// tell JPedal we want teasers
grouping.generateTeasers();
//allow us to add options
grouping.setIncludeHTML(true);
//Set search term in results list
final StringBuilder term = new StringBuilder();
for (int i = 0; i != searchTerms.length; i++) {
term.append(searchTerms[i]).append(' ');
}
resultsList.setSearchTerm(term.toString());
final SortedMap<Object, String> highlightsWithTeasers =
grouping.findTextWithinInAreaWithTeasers(x1, y1, x2, y2, pageSize.getRotation(page), searchTerms, searchTypeParameters, listener);
//update data structures with results from this page
if (!highlightsWithTeasers.isEmpty()) {
itemFoundCount += highlightsWithTeasers.size();
for (final Map.Entry<Object, String> e : highlightsWithTeasers.entrySet()) {
/*highlight is a rectangle or a rectangle[]*/
final Object highlight = e.getKey();
final String teaser = e.getValue();
// [AWI]: Only push the results off to the EDT if the model is the
// current active list model.
if (!SwingUtilities.isEventDispatchThread() && model == listModel) {
final Runnable setTextRun = () -> {
if (currentKey == searchKey) {
//if highights ensure displayed by wrapping in tags
if (!teaser.contains("<b>")) {
model.addElement(teaser);
} else {
model.addElement("<html>" + teaser + "</html>");
}
}
};
SwingUtilities.invokeLater(setTextRun);
} else {
if (!teaser.contains("<b>")) {
model.addElement(teaser);
} else {
model.addElement("<html>" + teaser + "</html>");
}
}
final Integer key = textRectangles.size();
textRectangles.put(key, highlight);
textPages.put(key, page);
if (page < firstPageWithResults || firstPageWithResults == 0) {
firstPageWithResults = page;
}
}
}
lastPage = page;
}
/**
* Get the first page with results
*
* @return int value for the page of the first result
*/
public int getFirstPageWithResults() {
return firstPageWithResults;
}
/**
* Have search text input grab focus
*/
public void grabFocusInInput() {
searchText.grabFocus();
}
/**
* Gets the visibility of the search interface (external window style only)
*
* @return true is visible, otherwise false
*/
public boolean isSearchVisible() {
return isVisible();
}
/**
* Set the view style for the search interface using a flag to control the style
* <p>
* For instance the search interface can be displayed as follows.
* setViewStyle(SwingSearchWindow.SEARCH_EXTERNAL_WINDOW); // Show search interface as external window
* setViewStyle(SwingSearchWindow.SEARCH_TABBED_PANE); // Show search interface on side tab bar
* setViewStyle(SwingSearchWindow.SEARCH_MENU_BAR); // Show search interface on button tool bar
*
* @param style int value representing the search style
*/
public void setViewStyle(final int style) {
this.style = style;
}
/**
* Get the view style being used by the search interface
*
* @return int value representing the search style
*/
public int getViewStyle() {
return style;
}
/**
* Set the value of the search input
*
* @param s String value to be used as search input
*/
public void setSearchText(final String s) {
deleteOnClick = false;
searchText.setText(s);
}
/**
* Get the search result areas for the current results
*
* @return Map object containing result areas using result index as keys
*/
public Map<Integer, Object> getTextRectangles() {
return Collections.unmodifiableMap(textRectangles);
}
/**
* Get the search result list
*
* @return SearchList containing all search result data
*/
public SearchList getResults() {
return resultsList;
}
/**
* Perform a search on a single page and return the results
*
* @param page int value for the page to be searched
* @return SearchList containing the results of the search
*/
public SearchList getResults(final int page) {
usingMenuBarSearch = style == SEARCH_MENU_BAR;
if (page != lastPage && usingMenuBarSearch) {
try {
searchKey++;
if (searchKey > 3_000) {
searchKey = 0;
}
clearCurrentResults();
// [AWI]: Updated for changes to searchPage method
searchPage(page, searchKey, listModel);
} catch (final Exception e) {
LogWriter.writeLog(e);
}
}
return resultsList;
}
/**
* Reset search text and menu bar buttons when opening new page
*/
public void resetSearchWindow() {
if (isSetup) {
searchText.setText(defaultMessage);
deleteOnClick = true;
// [AWI] Reset the result status text label (itemFoundCount should be reset already
// by the removeSearchWindow method)
searchCount.setText(Messages.getMessage("PdfViewerSearch.ItemsFound") + ' ' + itemFoundCount);
if (hasSearched) {
currentGUI.getButtons().getButton(Commands.NEXTRESULT).setEnabled(false);
currentGUI.getButtons().getButton(Commands.PREVIOUSRESULT).setEnabled(false);
hasSearched = false;
}
decode_pdf.requestFocus();
}
}
/**
* Dispose of the search window
*/
@Override
public void dispose() {
if (searchFuture != null) {
searchFuture.cancel(true);
clearCurrentResults();
}
if (resultsList != null) {
for (final ListSelectionListener listener : resultsList.getListSelectionListeners()) {
resultsList.removeListSelectionListener(listener);
}
}
resultsList = null;
if (searchButton != null) {
for (final ActionListener listener : searchButton.getActionListeners()) {
searchButton.removeActionListener(listener);
}
}
searchButton = null;
if (searchText != null) {
for (final KeyListener listener : searchText.getKeyListeners()) {
searchText.removeKeyListener(listener);
}
for (final FocusListener listener : searchText.getFocusListeners()) {
searchText.removeFocusListener(listener);
}
}
searchText = null;
if (label != null) {
for (final MouseListener listener : label.getMouseListeners()) {
label.removeMouseListener(listener);
}
}
label = null;
for (final WindowListener listener : getWindowListeners()) {
removeWindowListener(listener);
}
for (final PropertyChangeListener listener : getPropertyChangeListeners()) {
removePropertyChangeListener(listener);
}
for (final ComponentListener listener : getComponentListeners()) {
removeComponentListener(listener);
}
for (final MouseListener listener : getMouseListeners()) {
removeMouseListener(listener);
}
for (final FocusListener listener : getFocusListeners()) {
removeFocusListener(listener);
}
if (searchAreas != null) {
searchAreas.clear();
}
searchAreas = null;
if (listModel != null) {
listModel.clear();
}
listModel = null;
if (advancedPanel != null) {
for (final PropertyChangeListener listener : advancedPanel.getPropertyChangeListeners()) {
advancedPanel.removePropertyChangeListener(listener);
}
advancedPanel.removeAll();
}
advancedPanel = null;
removeAll();
decode_pdf = null;
service.shutdown();
super.dispose();
}
private int getSearchOptionsAsInt() {
int searchOptions = SearchType.DEFAULT;
if (wholeWordsOnlyBox.isSelected()) {
searchOptions |= SearchType.WHOLE_WORDS_ONLY;
}
if (caseSensitiveBox.isSelected()) {
searchOptions |= SearchType.CASE_SENSITIVE;
}
if (multiLineBox.isSelected()) {
searchOptions |= SearchType.MUTLI_LINE_RESULTS;
}
if (highlightAll.isSelected()) {
searchOptions |= SearchType.HIGHLIGHT_ALL_RESULTS;
}
if (useRegEx.isSelected()) {
searchOptions |= SearchType.USE_REGULAR_EXPRESSIONS;
}
if (ignoreWhiteSpace.isSelected()) {
searchOptions |= SearchType.IGNORE_SPACE_CHARACTERS;
}
if (searchHighlightedOnly.isSelected()) {
searchOptions |= SearchType.SEARCH_HIGHLIGHTS_ONLY;
}
return searchOptions;
}
/**
* [AWI] Focus listener used to detect when the search text field has been given focus.</p>
* <p>
* This class was added to support detecting when Keyboard input is needed for systems using a touchscreen monitor
* with a virtual keyboard.
*/
class SearchTextFocusListener extends FocusAdapter {
@Override
public void focusGained(final FocusEvent e) {
if (decode_pdf != null) {
final Object handler = decode_pdf.getExternalHandler(Options.KeyboardReadyHandler);
if (handler instanceof JPedalActionHandler) {
((JPedalActionHandler) handler).actionPerformed(currentGUI, null);
}
}
}
}
class searchListListener implements ListSelectionListener {
@Override
public void valueChanged(final ListSelectionEvent e) {
/*
* Only do something on mouse button up,
* prevents this code being called twice
* on mouse click
*/
if (!e.getValueIsAdjusting()) {
if (!Values.isProcessing()) {
final int id = resultsList.getSelectedIndex();
decode_pdf.getTextLines().clearHighlights();
if (id != -1) {
final Integer newPage = textPages.get(id);
if (newPage != null) {
//move to new page
if (commonValues.getCurrentPage() != newPage) {
PageNavigator.gotoPage(newPage.toString(), currentGUI, commonValues, decode_pdf);
}
while (Values.isProcessing()) {
//Ensure page has been processed else highlight may be incorrect
try {
Thread.sleep(500);
} catch (final InterruptedException e2) {
LogWriter.writeLog(e2);
}
}
//Highlight all search results on page.
if ((searchTypeParameters & SearchType.HIGHLIGHT_ALL_RESULTS) == SearchType.HIGHLIGHT_ALL_RESULTS) {
highlightAllSearchResults();
} else {
highlightSingleSearchResults(id);
}
//Refresh display after clearing highlights
decode_pdf.repaintPane(0);
decode_pdf.getPages().refreshDisplay();
}
}
}
//When page changes make sure only relevant navigation buttons are displayed
currentGUI.getButtons().setBackNavigationButtonsEnabled(commonValues.getCurrentPage() != 1);
currentGUI.getButtons().setForwardNavigationButtonsEnabled(commonValues.getCurrentPage() != decode_pdf.getPageCount());
} else {
resultsList.repaint();
}
}
private void highlightAllSearchResults() {
int[][] showAllOnPage;
Vector_Rectangle_Int storageVector = new Vector_Rectangle_Int();
int lastPage = -1;
for (int k = 0; k != resultsList.getModel().getSize(); k++) {
final Integer page = textPages.get(k);
if (page != null) {
final int currentPage = page;
if (currentPage != lastPage) {
storageVector.trim();
showAllOnPage = storageVector.get();
decode_pdf.getTextLines().addHighlights(showAllOnPage, true, lastPage);
lastPage = currentPage;
storageVector = new Vector_Rectangle_Int();
}
final Object highlight = textRectangles.get(k);
if (highlight instanceof int[]) {
storageVector.addElement((int[]) highlight);
}
if (highlight instanceof int[][]) {
final int[][] areas = (int[][]) highlight;
for (int i = 0; i != areas.length; i++) {
storageVector.addElement(areas[i]);
}
}
}
}
storageVector.trim();
showAllOnPage = storageVector.get();
decode_pdf.getTextLines().addHighlights(showAllOnPage, true, lastPage);
}
private void highlightSingleSearchResults(final int id) {
final int currentPage = textPages.get(id);
final Vector_Rectangle_Int storageVector = new Vector_Rectangle_Int();
int[] scroll = null;
final Object highlight = textRectangles.get(id);
if (highlight instanceof int[]) {
storageVector.addElement((int[]) highlight);
scroll = (int[]) highlight;
}
if (highlight instanceof int[][]) {
final int[][] areas = (int[][]) highlight;
scroll = areas[0];
for (int i = 0; i != areas.length; i++) {
storageVector.addElement(areas[i]);
}
}
if (scroll != null) {
Scroll.rectToHighlight(scroll, currentPage, decode_pdf);
}
storageVector.trim();
decode_pdf.getTextLines().addHighlights(storageVector.get(), true, currentPage);
}
}
}
|