true
, three notification methods are called asynchronously
* on a background thread. These are
* {@link Callback#quickSearchUpdate(java.lang.String)},
* {@link Callback#showNextSelection(javax.swing.text.Position.Bias)},
* {@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 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 (searchPanel != null) {
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 void fireQuickSearchUpdate(String searchText) {
if (asynchronous) {
rp.post(new LazyFire(QS_FIRE.UPDATE, searchText));
} else {
callback.quickSearchUpdate(searchText);
}
}
private void fireShowNextSelection(boolean forward) {
if (asynchronous) {
rp.post(new LazyFire(QS_FIRE.NEXT, forward));
} else {
callback.showNextSelection(forward);
}
}
private void findMaxPrefix(String prefix, DataContentHandlerFactory newPrefixSetter) {
if (asynchronous) {
rp.post(new LazyFire(QS_FIRE.MAX, prefix, newPrefixSetter));
} else {
prefix = callback.findMaxPrefix(prefix);
newPrefixSetter.createDataContentHandler(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);
searchTextField.getDocument().addDocumentListener(searchFieldListener);
}
private void displaySearchField() {
if (searchPanel != null || !isEnabled()) {
return;
}
searchTextField.setOriginalFocusOwner();
searchTextField.setFont(component.getFont());
searchPanel = new SearchPanel();
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) {
if (e != null && !SwingUtilities.isLeftMouseButton(e)) {
return;
}
JPopupMenu pm = popupMenu.getPopupMenu();
pm.show(lbl, 0, lbl.getHeight() - 1);
}
});
} 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));
searchPanel.add(lbl);
searchPanel.add(searchTextField);
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();
}
private void removeSearchField() {
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 final static 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 DataContentHandlerFactory 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,
DataContentHandlerFactory newPrefixSetter) {
this(fire, searchText, true, newPrefixSetter);
}
LazyFire(QS_FIRE fire, String searchText, boolean forward,
DataContentHandlerFactory 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.createDataContentHandler(mp);
break;
}
} finally {
animationTimer.stopProgressAnimation();
}
}
}
private static class SearchPanel extends JPanel {
public static final boolean isAquaLaF =
"Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N
public SearchPanel() {
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;
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 WeakReferencefalse
* it's called in EQ thread, otherwise, it's called in a background thread.
* 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.
* When {@link #isAsynchronous()} is false
* it's called in EQ thread, otherwise, it's called in a background thread.
* 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.
* When {@link #isAsynchronous()} is false
* it's called in EQ thread, otherwise, it's called in a background thread.
* 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();
}
}