org.openide.awt.QuickSearch Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide.awt;
import java.awt.*;
import java.awt.event.*;
import java.lang.ref.WeakReference;
import java.util.function.Consumer;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
import org.netbeans.api.annotations.common.StaticResource;
import org.openide.util.ImageUtilities;
import org.openide.util.RequestProcessor;
/**
* Quick search infrastructure for an arbitrary component.
* When quick search is attached to a component, it listens on key events going
* to the component and displays a quick search field.
*
* @author Martin Entlicher
* @since 7.43
*/
public class QuickSearch {
@StaticResource
private static final String ICON_FIND = "org/openide/awt/resources/quicksearch/find.png"; // NOI18N
@StaticResource
private static final String ICON_FIND_WITH_MENU = "org/openide/awt/resources/quicksearch/findMenu.png"; // NOI18N
private static final Object CLIENT_PROPERTY_KEY = new Object();
private final JComponent component;
private final Object constraints;
private final Callback callback;
private final JMenu popupMenu;
private final boolean asynchronous;
private boolean enabled = true;
private SearchTextField searchTextField;
private KeyAdapter quickSearchKeyAdapter;
private SearchFieldListener searchFieldListener;
private JPanel searchPanel;
private final RequestProcessor rp;
private static enum QS_FIRE { UPDATE, NEXT, MAX }
private AnimationTimer animationTimer;
private boolean alwaysShown = false;
private volatile boolean hasSearchText = false;
private QuickSearch(JComponent component, Object constraints,
Callback callback, boolean asynchronous, JMenu popupMenu) {
this.component = component;
this.constraints = constraints;
this.callback = callback;
this.asynchronous = asynchronous;
this.popupMenu = popupMenu;
if (asynchronous) {
rp = new RequestProcessor(QuickSearch.class);
} else {
rp = null;
}
setUpSearch();
}
/**
* Attach quick search to a component with given constraints.
* It listens on key events going to the component and displays a quick search
* field.
*
* @param component The component to attach to
* @param constraints The constraints that are used to add the search field
* to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
* when adding the quick search UI to the component.
* @param callback The call back implementation, which is notified from the
* quick search field submissions.
* @return An instance of QuickSearch class.
*/
public static QuickSearch attach(JComponent component, Object constraints,
Callback callback) {
return attach(component, constraints, callback, false, null);
}
/**
* Attach quick search to a component with given constraints.
* It listens on key events going to the component and displays a quick search
* field.
*
* @param component The component to attach to
* @param constraints The constraints that are used to add the search field
* to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
* when adding the quick search UI to the component.
* @param callback The call back implementation, which is notified from the
* quick search field submissions.
* @return An instance of QuickSearch class.
*/
public static QuickSearch attach(JComponent component, Object constraints,
Callback callback, boolean asynchronous) {
return attach(component, constraints, callback, asynchronous, null);
}
/**
* Attach quick search to a component with given constraints.
* It listens on key events going to the component and displays a quick search
* field.
*
* @param component The component to attach to
* @param constraints The constraints that are used to add the search field
* to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
* when adding the quick search UI to the component.
* @param callback The call back implementation, which is notified from the
* quick search field submissions.
* @param popupMenu A pop-up menu, that is displayed on the find icon, next to the search
* field. This allows customization of the search criteria. The pop-up menu
* is taken from {@link JMenu#getPopupMenu()}.
* @return An instance of QuickSearch class.
*/
public static QuickSearch attach(JComponent component, Object constraints,
Callback callback, JMenu popupMenu) {
return attach(component, constraints, callback, false, popupMenu);
}
/**
* Attach quick search to a component with given constraints.
* It listens on key events going to the component and displays a quick search
* field.
*
* @param component The component to attach to
* @param constraints The constraints that are used to add the search field
* to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
* when adding the quick search UI to the component.
* @param callback The call back implementation, which is notified from the
* quick search field submissions.
* @param asynchronous Set whether the quick search notifies the call back
* asynchronously, or not.
* By default, Callback is notified synchronously on EQ thread.
* If true
, three notification methods are called asynchronously
* on a background thread. These are
* {@link Callback#quickSearchUpdate(java.lang.String)},
* {@link Callback#showNextSelection(boolean)},
* {@link Callback#findMaxPrefix(java.lang.String)}.
* If false
all methods are called synchronously on EQ thread.
* @param popupMenu A pop-up menu, that is displayed on the find icon, next to the search
* field. This allows customization of the search criteria. The pop-up menu
* is taken from {@link JMenu#getPopupMenu()}.
* @return An instance of QuickSearch class.
*/
public static QuickSearch attach(JComponent component, Object constraints,
Callback callback, boolean asynchronous, JMenu popupMenu) {
Object qso = component.getClientProperty(CLIENT_PROPERTY_KEY);
if (qso instanceof QuickSearch) {
throw new IllegalStateException("A quick search is attached to this component already, detach it first."); // NOI18N
} else {
QuickSearch qs = new QuickSearch(component, constraints, callback, asynchronous, popupMenu);
component.putClientProperty(CLIENT_PROPERTY_KEY, qs);
return qs;
}
}
/**
* Detach the quick search from the component it was attached to.
*/
public void detach() {
setEnabled(false);
component.putClientProperty(CLIENT_PROPERTY_KEY, null);
}
/**
* Test whether the quick search field is always shown.
* This is false
by default.
* @return true
when the search field is always shown,
* false
otherwise.
* @since 7.49
*/
public boolean isAlwaysShown() {
return alwaysShown;
}
/**
* Set whether the quick search field should always be shown.
* @param alwaysShown true
to always show the search field,
* false
otherwise.
* @since 7.49
*/
public void setAlwaysShown(boolean alwaysShown) {
this.alwaysShown = alwaysShown;
if(alwaysShown) {
displaySearchField();
} else {
removeSearchField();
}
}
/**
* Test whether the quick search is enabled. This is true
* by default.
* @return true
when the quick search is enabled,
* false
otherwise.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Set the enabled state of the quick search.
* This allows to activate/deactivate the quick search functionality.
* @param enabled true
to enable the quick search,
* false
otherwise.
*/
public void setEnabled(boolean enabled) {
if (this.enabled == enabled) {
return ;
}
this.enabled = enabled;
if (enabled) {
component.addKeyListener(quickSearchKeyAdapter);
} else {
removeSearchField();
component.removeKeyListener(quickSearchKeyAdapter);
}
}
/**
* Process this key event in addition to the key events obtained from the
* component we're attached to.
* @param ke a key event to process.
*/
public void processKeyEvent(KeyEvent ke) {
if (!isEnabled()) {
return ;
}
if (searchPanel != null) {
if (!isKeyEventInSearchFieldIgnored(ke)) {
searchTextField.setCaretPosition(searchTextField.getText().length());
searchTextField.processKeyEvent(ke);
}
} else {
switch(ke.getID()) {
case KeyEvent.KEY_PRESSED:
quickSearchKeyAdapter.keyPressed(ke);
break;
case KeyEvent.KEY_RELEASED:
quickSearchKeyAdapter.keyReleased(ke);
break;
case KeyEvent.KEY_TYPED:
quickSearchKeyAdapter.keyTyped(ke);
break;
}
}
}
private boolean isKeyEventInSearchFieldIgnored(KeyEvent ke) {
// Ignore DELETE key events unless the search field has focus
if (ke.getKeyCode() == KeyEvent.VK_DELETE) {
return !searchTextField.isFocusOwner();
} else {
return false;
}
}
private void fireQuickSearchUpdate(String searchText) {
if (asynchronous) {
rp.post(new LazyFire(QS_FIRE.UPDATE, searchText));
} else {
callback.quickSearchUpdate(searchText);
}
hasSearchText = true;
}
private void fireShowNextSelection(boolean forward) {
if (asynchronous) {
rp.post(new LazyFire(QS_FIRE.NEXT, forward));
} else {
callback.showNextSelection(forward);
}
}
private void findMaxPrefix(String prefix, Consumer newPrefixSetter) {
if (asynchronous) {
rp.post(new LazyFire(QS_FIRE.MAX, prefix, newPrefixSetter));
} else {
prefix = callback.findMaxPrefix(prefix);
newPrefixSetter.accept(prefix);
}
}
private void setUpSearch() {
searchTextField = new SearchTextField();
// create new key listeners
quickSearchKeyAdapter = (
new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
int modifiers = e.getModifiers();
int keyCode = e.getKeyCode();
char c = e.getKeyChar();
//#43617 - don't eat + and -
//#98634 - and all its duplicates dont't react to space
if ((c == '+') || (c == '-') || (c==' ')) return; // NOI18N
if (((modifiers > 0) && (modifiers != KeyEvent.SHIFT_MASK)) || e.isActionKey()) {
return;
}
if (Character.isISOControl(c) ||
(keyCode == KeyEvent.VK_SHIFT) ||
(keyCode == KeyEvent.VK_ESCAPE)) return;
displaySearchField();
final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
searchTextField.setText(String.valueOf(stroke.getKeyChar()));
e.consume();
}
}
);
if (isEnabled()) {
component.addKeyListener(quickSearchKeyAdapter);
}
// Create a the "multi-event" listener for the text field. Instead of
// adding separate instances of each needed listener, we're using a
// class which implements them all. This approach is used in order
// to avoid the creation of 4 instances which takes some time
searchFieldListener = new SearchFieldListener();
searchTextField.addKeyListener(searchFieldListener);
searchTextField.addFocusListener(searchFieldListener);
Document searchDoc = searchTextField.getDocument();
searchDoc.addDocumentListener(searchFieldListener);
if (searchDoc instanceof AbstractDocument) {
((AbstractDocument) searchDoc).setDocumentFilter(searchFieldListener.new ReplaceFilter());
}
if(isAlwaysShown()) {
displaySearchField();
}
}
private void displaySearchField() {
if (searchPanel != null || !isEnabled()) {
return;
}
searchTextField.setOriginalFocusOwner();
searchTextField.setFont(component.getFont());
searchPanel = new SearchPanel(component, isAlwaysShown());
final JLabel lbl;
if (popupMenu != null) {
lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND_WITH_MENU, false));
lbl.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
maybeShowPopup(e, lbl);
}
});
} else {
lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND, false));
}
if (asynchronous) {
animationTimer = new AnimationTimer(lbl, lbl.getIcon());
} else {
animationTimer = null;
}
searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS));
if ("Aqua".equals(UIManager.getLookAndFeel().getID())) { //NOI18N
if (popupMenu != null) {
final JPopupMenu dummy = new JPopupMenu();
dummy.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
dummy.setVisible(false);
}
});
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
});
searchTextField.putClientProperty("JTextField.Search.FindPopup", dummy); //NOI18N
searchTextField.putClientProperty("JTextField.Search.FindAction", new ActionListener() { //NOI18N
@Override
public void actionPerformed(ActionEvent e) {
maybeShowPopup(null, searchTextField);
}
});
}
} else {
searchPanel.add(lbl);
}
searchPanel.add(searchTextField);
searchPanel.setBackground(component.getBackground());
lbl.setLabelFor(searchTextField);
searchTextField.setColumns(10);
searchTextField.setMaximumSize(searchTextField.getPreferredSize());
searchTextField.putClientProperty("JTextField.variant", "search"); //NOI18N
lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
if (constraints == null) {
component.add(searchPanel);
} else {
component.add(searchPanel, constraints);
}
component.invalidate();
component.revalidate();
component.repaint();
searchTextField.requestFocus();
searchTextField.selectAll(); // Select an existing text for an easy rewrite
}
protected void maybeShowPopup(MouseEvent evt, Component comp) {
if (evt != null && !SwingUtilities.isLeftMouseButton(evt)) {
return;
}
JPopupMenu pm = popupMenu.getPopupMenu();
pm.show(comp, 0, comp.getHeight() - 1);
searchTextField.setText("");
searchTextField.requestOriginalFocusOwner();
}
private void removeSearchField() {
if (isAlwaysShown()) {
return;
}
if (searchPanel == null) {
return;
}
if (animationTimer != null) {
animationTimer.stopProgressAnimation();
}
Component sp = searchPanel;
searchPanel = null;
component.remove(sp);
component.invalidate();
component.revalidate();
component.repaint();
}
/** Accessed from test. */
JTextField getSearchField() {
return searchTextField;
}
/**
* Utility method, that finds a greatest common prefix of two supplied
* strings.
*
* @param str1 The first string
* @param str2 The second string
* @param ignoreCase Whether to ignore case in the comparisons
* @return The greatest common prefix of the two strings.
*/
public static String findMaxPrefix(String str1, String str2, boolean ignoreCase) {
int n1 = str1.length();
int n2 = str2.length();
int i = 0;
if (ignoreCase) {
for ( ; i < n1 && i < n2; i++) {
char c1 = Character.toUpperCase(str1.charAt(i));
char c2 = Character.toUpperCase(str2.charAt(i));
if (c1 != c2) {
break;
}
}
} else {
for ( ; i < n1 && i < n2; i++) {
char c1 = str1.charAt(i);
char c2 = str2.charAt(i);
if (c1 != c2) {
break;
}
}
}
return str1.substring(0, i);
}
private static final class AnimationTimer {
private final JLabel jLabel;
private final Icon findIcon;
private final Timer animationTimer;
public AnimationTimer(final JLabel jLabel, Icon findIcon) {
this.jLabel = jLabel;
this.findIcon = findIcon;
animationTimer = new Timer(100, new ActionListener() {
ImageIcon icons[];
int index = 0;
@Override
public void actionPerformed(ActionEvent e) {
if (icons == null) {
icons = new ImageIcon[8];
for (int i = 0; i < 8; i++) {
icons[i] = ImageUtilities.loadImageIcon("org/openide/awt/resources/quicksearch/progress_" + i + ".png", false); //NOI18N
}
}
jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 6));
jLabel.setIcon(icons[index]);
//mac os x
jLabel.repaint();
index = (index + 1) % 8;
}
});
}
public void startProgressAnimation() {
if (animationTimer != null && !animationTimer.isRunning()) {
animationTimer.start();
}
}
public void stopProgressAnimation() {
if (animationTimer != null && animationTimer.isRunning()) {
animationTimer.stop();
jLabel.setIcon(findIcon);
jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
}
}
}
private class LazyFire implements Runnable {
private final QS_FIRE fire;
//private final QuickSearchListener[] qsls;
private final String searchText;
private final boolean forward;
private final Consumer newPrefixSetter;
LazyFire(QS_FIRE fire, String searchText) {
this(fire, searchText, true, null);
}
LazyFire(QS_FIRE fire, boolean forward) {
this(fire, null, forward);
}
LazyFire(QS_FIRE fire, String searchText, boolean forward) {
this(fire, searchText, forward, null);
}
LazyFire(QS_FIRE fire, String searchText,
Consumer newPrefixSetter) {
this(fire, searchText, true, newPrefixSetter);
}
LazyFire(QS_FIRE fire, String searchText, boolean forward,
Consumer newPrefixSetter) {
this.fire = fire;
//this.qsls = qsls;
this.searchText = searchText;
this.forward = forward;
this.newPrefixSetter = newPrefixSetter;
animationTimer.startProgressAnimation();
}
@Override
public void run() {
try {
switch (fire) {
case UPDATE: callback.quickSearchUpdate(searchText);//fireQuickSearchUpdate(qsls, searchText);
break;
case NEXT: callback.showNextSelection(forward);//fireShowNextSelection(qsls, forward);
break;
case MAX: String mp = callback.findMaxPrefix(searchText);//String mp = findMaxPrefix(qsls, searchText);
newPrefixSetter.accept(mp);
break;
}
} finally {
animationTimer.stopProgressAnimation();
}
}
}
private static class SearchPanel extends JPanel {
public static final boolean isAquaLaF =
"Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N
private JComponent component;
private boolean alwaysShown = false;
public SearchPanel(JComponent component, boolean alwaysShown) {
this.component = component;
this.alwaysShown = alwaysShown;
if (isAquaLaF) {
setBorder(BorderFactory.createEmptyBorder(9,6,8,2));
} else {
setBorder(BorderFactory.createEmptyBorder(2,6,2,2));
}
setOpaque(true);
}
@Override
protected void paintComponent(Graphics g) {
if (isAquaLaF && g instanceof Graphics2D) {
Graphics2D g2d = (Graphics2D) g;
if (alwaysShown) {
g2d.setColor(component.getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
} else {
g2d.setPaint(new GradientPaint(0, 0, UIManager.getColor("NbExplorerView.quicksearch.background.top"), //NOI18N
0, getHeight(), UIManager.getColor("NbExplorerView.quicksearch.background.bottom")));//NOI18N
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setColor(UIManager.getColor("NbExplorerView.quicksearch.border")); //NOI18N
g2d.drawLine(0, 0, getWidth(), 0);
}
} else {
super.paintComponent(g);
}
}
}
/** searchTextField manages focus because it handles VK_ESCAPE key */
private class SearchTextField extends JTextField {
private WeakReference originalFocusOwner = new WeakReference(null);
public SearchTextField() {
}
void setOriginalFocusOwner() {
Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
if (focusOwner != null && component.isAncestorOf(focusOwner)) {
originalFocusOwner = new WeakReference(focusOwner);
} else {
originalFocusOwner = new WeakReference(component);
}
}
void requestOriginalFocusOwner() {
SwingUtilities.invokeLater(
new Runnable() {
//additional bugfix - do focus change later or removing
//the component while it's focused will cause focus to
//get transferred to the next component in the
//parent focusTraversalPolicy *after* our request
//focus completes, so focus goes into a black hole - Tim
@Override
public void run() {
Component fo = originalFocusOwner.get();
if (fo != null) {
fo.requestFocusInWindow();
}
}
}
);
}
@Override
public boolean isManagingFocus() {
return true;
}
@Override
public void processKeyEvent(KeyEvent ke) {
//override the default handling so that
//the parent will never receive the escape key and
//close a modal dialog
if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) {
removeSearchField();
ke.consume();
searchFieldListener.ignoreEvents = true;
try {
// Clear the text after ESC
setText(""); // NOI18N
} finally {
searchFieldListener.ignoreEvents = false;
}
// bugfix #32909, reqest focus when search field is removed
requestOriginalFocusOwner();
//fireQuickSearchCanceled();
callback.quickSearchCanceled();
hasSearchText = false;
} else {
if (!hasSearchText) {
int keyCode = ke.getKeyCode();
if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_UP ||
keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_RIGHT ||
keyCode == KeyEvent.VK_TAB || keyCode == KeyEvent.VK_F3) {
// Ignore movement events when search text was not set
return ;
}
}
super.processKeyEvent(ke);
}
}
};
private class SearchFieldListener extends KeyAdapter implements DocumentListener, FocusListener {
private boolean ignoreEvents;
private boolean ignoreRemove;
SearchFieldListener() {
}
@Override
public void changedUpdate(DocumentEvent e) {
if (ignoreEvents) return;
searchForNode();
}
@Override
public void insertUpdate(DocumentEvent e) {
if (ignoreEvents) return;
searchForNode();
}
@Override
public void removeUpdate(DocumentEvent e) {
if (ignoreEvents || ignoreRemove) return;
searchForNode();
}
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_ESCAPE) {
removeSearchField();
searchTextField.requestOriginalFocusOwner();
ignoreEvents = true;
try {
// Clear the text after ESC
searchTextField.setText(""); // NOI18N
} finally {
ignoreEvents = false;
}
//fireQuickSearchCanceled();
callback.quickSearchCanceled();
hasSearchText = false;
e.consume();
} else if (keyCode == KeyEvent.VK_UP || (keyCode == KeyEvent.VK_F3 && e.isShiftDown())) {
fireShowNextSelection(false);
// Stop processing the event here. Otherwise it's dispatched
// to the tree too (which scrolls)
e.consume();
} else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_F3) {
fireShowNextSelection(true);
// Stop processing the event here. Otherwise it's dispatched
// to the tree too (which scrolls)
e.consume();
} else if (keyCode == KeyEvent.VK_TAB) {
findMaxPrefix(searchTextField.getText(), new Consumer() {
@Override
public void accept(final String maxPrefix) {
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
accept(maxPrefix);
searchTextField.transferFocus();
}
});
return ;
}
ignoreEvents = true;
try {
searchTextField.setText(maxPrefix);
} finally {
ignoreEvents = false;
}
}
});
e.consume();
} else if (keyCode == KeyEvent.VK_ENTER) {
removeSearchField();
//fireQuickSearchConfirmed();
callback.quickSearchConfirmed();
component.requestFocusInWindow();
e.consume();
}
}
/** Searches for a node in the tree. */
private void searchForNode() {
String text = searchTextField.getText();
if (text.isEmpty() && isAlwaysShown()) {
callback.quickSearchCanceled();
hasSearchText = false;
} else {
fireQuickSearchUpdate(text);
}
}
@Override
public void focusGained(FocusEvent e) {
if (e.getSource() == searchTextField) {
// make sure nothing is selected
int n = searchTextField.getText().length();
searchTextField.select(n, n);
}
}
@Override
public void focusLost(FocusEvent e) {
if (e.isTemporary() || isAlwaysShown()) {
return ;
}
Component oppositeComponent = e.getOppositeComponent();
if (e.getSource() != searchTextField) {
((Component) e.getSource()).removeFocusListener(this);
}
if (oppositeComponent instanceof JMenuItem || oppositeComponent instanceof JPopupMenu) {
oppositeComponent.addFocusListener(this);
return ;
}
if (oppositeComponent == searchTextField) {
return ;
}
if (searchPanel != null) {
removeSearchField();
//fireQuickSearchConfirmed();
callback.quickSearchCanceled();
hasSearchText = false;
}
}
private class ReplaceFilter extends DocumentFilter {
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
// Replace might do remove before insert. Suppress remove so that we get only one event with the final text
if (text != null && !text.isEmpty()) {
// will insert
ignoreRemove = true;
}
try {
super.replace(fb, offset, length, text, attrs);
} finally {
ignoreRemove = false;
}
}
}
}
/**
* Call back interface, that is notified with the submissions to the quick search field.
*
* @author Martin Entlicher
* @since 7.43
*/
public static interface Callback {
/**
* Called with an updated search text.
* The client should update the visual representation of the search results
* and then return.
* This method is called to initiate and update the search process.
* @param searchText The new text to search for.
*/
void quickSearchUpdate(String searchText);
/**
* Called to select a next occurrence of the search result.
* The client should update the visual representation of the search results
* and then return.
* @param forward The direction of the next search result.
* true
for forward direction,
* false
for backward direction.
*/
void showNextSelection(boolean forward);
/**
* Find the maximum prefix among the search results, that starts with the provided string.
* This method is called when user press TAB in the search field, to auto-complete
* the maximum prefix.
* Utility method {@link QuickSearch#findMaxPrefix(java.lang.String, java.lang.String, boolean)}
* can be used by the implementation.
* @param prefix The prefix to start with
* @return The maximum prefix.
*/
String findMaxPrefix(String prefix);
/**
* Called when the quick search is confirmed by the user.
* This method is called in EQ thread always.
*/
void quickSearchConfirmed();
/**
* Called when the quick search is canceled by the user.
* This method is called in EQ thread always.
*/
void quickSearchCanceled();
}
}