diff --git a/openide.awt/apichanges.xml b/openide.awt/apichanges.xml --- a/openide.awt/apichanges.xml +++ b/openide.awt/apichanges.xml @@ -50,6 +50,20 @@ AWT API + + + QuickSearch class that allows to attach quick search field to an arbitratry component. + + + + + QuickSearch class is added. It can be used to attach + a quick search functionality to an arbitrary component. + + + + Added Actions.forID diff --git a/openide.awt/manifest.mf b/openide.awt/manifest.mf --- a/openide.awt/manifest.mf +++ b/openide.awt/manifest.mf @@ -2,5 +2,5 @@ OpenIDE-Module: org.openide.awt OpenIDE-Module-Localizing-Bundle: org/openide/awt/Bundle.properties AutoUpdate-Essential-Module: true -OpenIDE-Module-Specification-Version: 7.42 +OpenIDE-Module-Specification-Version: 7.43 diff --git a/openide.awt/nbproject/project.xml b/openide.awt/nbproject/project.xml --- a/openide.awt/nbproject/project.xml +++ b/openide.awt/nbproject/project.xml @@ -50,6 +50,15 @@ org.openide.awt + org.netbeans.api.annotations.common + + + + 1 + 1.13 + + + org.openide.filesystems diff --git a/openide.explorer/src/org/openide/explorer/view/QuickSearch.java b/openide.awt/src/org/openide/awt/QuickSearch.java copy from openide.explorer/src/org/openide/explorer/view/QuickSearch.java copy to openide.awt/src/org/openide/awt/QuickSearch.java --- a/openide.explorer/src/org/openide/explorer/view/QuickSearch.java +++ b/openide.awt/src/org/openide/awt/QuickSearch.java @@ -39,63 +39,130 @@ * * Portions Copyrighted 2012 Sun Microsystems, Inc. */ -package org.openide.explorer.view; +package org.openide.awt; import java.awt.*; import java.awt.event.*; import java.lang.ref.WeakReference; import java.util.LinkedList; import java.util.List; +import javax.activation.DataContentHandler; +import javax.activation.DataContentHandlerFactory; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import javax.swing.text.Position.Bias; +import org.netbeans.api.annotations.common.StaticResource; +import org.openide.util.ImageUtilities; +import org.openide.util.RequestProcessor; /** - * Quick search infrastructure + * 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 */ -class QuickSearch { +public class QuickSearch { - private static final String ICON_FIND = "org/openide/explorer/view/find.png"; - private static final String ICON_FIND_WITH_MENU = "org/openide/explorer/view/findMenu.png"; + @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 boolean enabled = true; - private final List listeners = new LinkedList(); private SearchTextField searchTextField; private KeyAdapter quickSearchKeyAdapter; + private SearchFieldListener searchFieldListener; private JPanel searchPanel; - private JMenu popupMenu; + private RequestProcessor rp; + private static enum QS_FIRE { UPDATE, NEXT, MAX } + private AnimationTimer animationTimer; - private QuickSearch(JComponent component, Object constraints) { + private QuickSearch(JComponent component, Object constraints, + Callback callback, JMenu popupMenu) { this.component = component; this.constraints = constraints; + this.callback = callback; + this.popupMenu = popupMenu; setUpSearch(); } - public static QuickSearch attach(JComponent component, Object constraints) { - Object qso = component.getClientProperty(QuickSearch.class.getName()); + /** + * 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, 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) { + Object qso = component.getClientProperty(CLIENT_PROPERTY_KEY); if (qso instanceof QuickSearch) { - return (QuickSearch) qso; + throw new IllegalStateException("A quick search is attached to this component already, detach it first."); // NOI18N } else { - QuickSearch qs = new QuickSearch(component, constraints); - component.putClientProperty(QuickSearch.class.getName(), qs); + QuickSearch qs = new QuickSearch(component, constraints, callback, 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(QuickSearch.class.getName(), null); + 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 ; @@ -104,76 +171,64 @@ if (enabled) { component.addKeyListener(quickSearchKeyAdapter); } else { + removeSearchField(); component.removeKeyListener(quickSearchKeyAdapter); } } - public void addQuickSearchListener(QuickSearchListener qsl) { - synchronized (listeners) { - listeners.add(qsl); + /** + * 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; + } } } - public void removeQuickSearchListener(QuickSearchListener qsl) { - synchronized (listeners) { - listeners.remove(qsl); + private RequestProcessor getRP() { + if (rp == null) { + rp = new RequestProcessor(QuickSearch.class); + } + return rp; + } + + private void fireQuickSearchUpdate(String searchText) { + if (callback.asynchronous()) { + getRP().post(new LazyFire(QS_FIRE.UPDATE, searchText)); + } else { + callback.quickSearchUpdate(searchText); } } - public void setPopupMenu(JMenu popupMenu) { - this.popupMenu = popupMenu; - } - - public void processKeyEvent(KeyEvent ke) { - 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 fireShowNextSelection(boolean forward) { + if (callback.asynchronous()) { + getRP().post(new LazyFire(QS_FIRE.NEXT, forward)); + } else { + callback.showNextSelection(forward); } } - private QuickSearchListener[] getQuickSearchListeners() { - QuickSearchListener[] qsls; - synchronized (listeners) { - qsls = listeners.toArray(new QuickSearchListener[] {}); - } - return qsls; - } - - private void fireQuickSearchUpdate(String searchText) { - for (QuickSearchListener qsl : getQuickSearchListeners()) { - qsl.quickSearchUpdate(searchText); - } - } - - private void fireShowNextSelection(Bias bias) { - for (QuickSearchListener qsl : getQuickSearchListeners()) { - qsl.showNextSelection(bias); - } - } - - private String findMaxPrefix(String prefix) { - for (QuickSearchListener qsl : getQuickSearchListeners()) { - prefix = qsl.findMaxPrefix(prefix); - } - return prefix; - } - - private void fireQuickSearchConfirmed() { - for (QuickSearchListener qsl : getQuickSearchListeners()) { - qsl.quickSearchConfirmed(); - } - } - - private void fireQuickSearchCanceled() { - for (QuickSearchListener qsl : getQuickSearchListeners()) { - qsl.quickSearchCanceled(); + private void findMaxPrefix(String prefix, DataContentHandlerFactory newPrefixSetter) { + if (callback.asynchronous()) { + getRP().post(new LazyFire(QS_FIRE.MAX, prefix, newPrefixSetter)); + } else { + prefix = callback.findMaxPrefix(prefix); + newPrefixSetter.createDataContentHandler(prefix); } } @@ -200,22 +255,23 @@ (keyCode == KeyEvent.VK_SHIFT) || (keyCode == KeyEvent.VK_ESCAPE)) return; + displaySearchField(); + final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e); searchTextField.setText(String.valueOf(stroke.getKeyChar())); - displaySearchField(); e.consume(); } } ); - if(isEnabled()){ + 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 searchFieldListener = new SearchFieldListener(); + searchFieldListener = new SearchFieldListener(); searchTextField.addKeyListener(searchFieldListener); searchTextField.addFocusListener(searchFieldListener); searchTextField.getDocument().addDocumentListener(searchFieldListener); @@ -226,19 +282,9 @@ if (searchPanel != null || !isEnabled()) { return; } - /* - TreeView previousSearchField = lastSearchField.get(); - if (previousSearchField != null && previousSearchField != this) { - previousSearchField.removeSearchField(); - } - */ - //JViewport vp = getViewport(); - //originalScrollMode = vp.getScrollMode(); - //vp.setScrollMode(JViewport.SIMPLE_SCROLL_MODE); searchTextField.setOriginalFocusOwner(); searchTextField.setFont(component.getFont()); searchPanel = new SearchPanel(); - //JLabel lbl = new JLabel(NbBundle.getMessage(TreeView.class, "LBL_QUICKSEARCH")); //NOI18N final JLabel lbl; if (popupMenu != null) { lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND_WITH_MENU, false)); @@ -255,6 +301,11 @@ } else { lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND, false)); } + if (callback.asynchronous()) { + animationTimer = new AnimationTimer(lbl, lbl.getIcon()); + } else { + animationTimer = null; + } searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS)); searchPanel.add(lbl); searchPanel.add(searchTextField); @@ -263,12 +314,6 @@ searchTextField.setMaximumSize(searchTextField.getPreferredSize()); searchTextField.putClientProperty("JTextField.variant", "search"); //NOI18N lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5)); - //JToggleButton matchCaseButton = new JToggleButton("aA"); - //matchCaseButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); - //searchPanel.add(matchCaseButton); - if (component instanceof JScrollPane) { - // ((JScrollPane) component).getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE); - } if (constraints == null) { component.add(searchPanel); } else { @@ -284,15 +329,32 @@ if (searchPanel == null) { return; } - component.remove(searchPanel); + if (animationTimer != null) { + animationTimer.stopProgressAnimation(); + } + Component sp = searchPanel; searchPanel = null; - //getViewport().setScrollMode(originalScrollMode); + component.remove(sp); component.invalidate(); component.revalidate(); component.repaint(); } - public static String findMaxCommonSubstring(String str1, String str2, boolean ignoreCase) { + /** 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; @@ -315,25 +377,115 @@ } 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() { - public static interface QuickSearchListener { + 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; + } + }); + } - void quickSearchUpdate(String searchText); + 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 { - void showNextSelection(Bias bias); + private final QS_FIRE fire; + //private final QuickSearchListener[] qsls; + private final String searchText; + private final boolean forward; + private final DataContentHandlerFactory newPrefixSetter; - String findMaxPrefix(String prefix); + LazyFire(QS_FIRE fire, String searchText) { + this(fire, searchText, true, null); + } - void quickSearchConfirmed(); + LazyFire(QS_FIRE fire, boolean forward) { + this(fire, null, forward); + } - void quickSearchCanceled(); + 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 (ViewUtil.isAquaLaF) { + if (isAquaLaF) { setBorder(BorderFactory.createEmptyBorder(9,6,8,2)); } else { setBorder(BorderFactory.createEmptyBorder(2,6,2,2)); @@ -343,9 +495,9 @@ @Override protected void paintComponent(Graphics g) { - if (ViewUtil.isAquaLaF && g instanceof Graphics2D) { + if (isAquaLaF && g instanceof Graphics2D) { Graphics2D g2d = (Graphics2D) g; - g2d.setPaint(new GradientPaint(0, 0, UIManager.getColor("NbExplorerView.quicksearch.background.top"), + 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 @@ -407,7 +559,8 @@ ke.consume(); // bugfix #32909, reqest focus when search field is removed requestOriginalFocusOwner(); - fireQuickSearchCanceled(); + //fireQuickSearchCanceled(); + callback.quickSearchCanceled(); } else { super.processKeyEvent(ke); } @@ -446,31 +599,47 @@ if (keyCode == KeyEvent.VK_ESCAPE) { removeSearchField(); searchTextField.requestOriginalFocusOwner(); - fireQuickSearchCanceled(); + //fireQuickSearchCanceled(); + callback.quickSearchCanceled(); e.consume(); } else if (keyCode == KeyEvent.VK_UP || (keyCode == KeyEvent.VK_F3 && e.isShiftDown())) { - fireShowNextSelection(Bias.Backward); + 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(Bias.Forward); + 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) { - String maxPrefix = findMaxPrefix(searchTextField.getText()); - ignoreEvents = true; - try { - searchTextField.setText(maxPrefix); - } finally { - ignoreEvents = false; - } + findMaxPrefix(searchTextField.getText(), new DataContentHandlerFactory() { + @Override + public DataContentHandler createDataContentHandler(final String maxPrefix) { + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + createDataContentHandler(maxPrefix); + } + }); + return null; + } + ignoreEvents = true; + try { + searchTextField.setText(maxPrefix); + } finally { + ignoreEvents = false; + } + return null; + } + }); e.consume(); } else if (keyCode == KeyEvent.VK_ENTER) { removeSearchField(); - fireQuickSearchConfirmed(); + //fireQuickSearchConfirmed(); + callback.quickSearchConfirmed(); component.requestFocusInWindow(); e.consume(); @@ -506,9 +675,85 @@ if (oppositeComponent == searchTextField) { return ; } - removeSearchField(); - fireQuickSearchConfirmed(); + if (searchPanel != null) { + removeSearchField(); + //fireQuickSearchConfirmed(); + callback.quickSearchConfirmed(); + } } } + + /** + * Call back interface, that is notified with the submissions to the quick search field. + * + * @author Martin Entlicher + * @since 7.43 + */ + public static interface Callback { + + /** + * Test whether the quick search notifies this 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 #quickSearchUpdate(java.lang.String)}, + * {@link #showNextSelection(javax.swing.text.Position.Bias)}, + * {@link #findMaxPrefix(java.lang.String)}. + * + * @return false for synchronous notification, + * true for asynchronous notification. + */ + boolean asynchronous(); + + /** + * Called with an updated search text. + * 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.

+ * 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(); + + } } diff --git a/openide.explorer/src/org/openide/explorer/view/find.png b/openide.awt/src/org/openide/awt/resources/quicksearch/find.png copy from openide.explorer/src/org/openide/explorer/view/find.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/find.png diff --git a/openide.explorer/src/org/openide/explorer/view/findMenu.png b/openide.awt/src/org/openide/awt/resources/quicksearch/findMenu.png copy from openide.explorer/src/org/openide/explorer/view/findMenu.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/findMenu.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_0.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_0.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_0.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_0.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_1.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_1.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_1.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_1.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_2.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_2.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_2.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_2.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_3.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_3.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_3.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_3.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_4.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_4.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_4.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_4.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_5.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_5.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_5.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_5.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_6.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_6.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_6.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_6.png diff --git a/spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_7.png b/openide.awt/src/org/openide/awt/resources/quicksearch/progress_7.png copy from spi.quicksearch/src/org/netbeans/modules/quicksearch/resources/progress_7.png copy to openide.awt/src/org/openide/awt/resources/quicksearch/progress_7.png diff --git a/openide.awt/test/unit/src/org/openide/awt/QuickSearchTest.java b/openide.awt/test/unit/src/org/openide/awt/QuickSearchTest.java new file mode 100644 --- /dev/null +++ b/openide.awt/test/unit/src/org/openide/awt/QuickSearchTest.java @@ -0,0 +1,699 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2012 Sun Microsystems, Inc. + */ +package org.openide.awt; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.BeforeClass; + +/** + * Test of QuickSearch. + * + * @author Martin Entlicher + */ +public class QuickSearchTest { + + public QuickSearchTest() { + } + + @BeforeClass + public static void setUpClass() throws Exception { + } + + @AfterClass + public static void tearDownClass() throws Exception { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of attach and detach methods, of class QuickSearch. + */ + @Test + public void testAttachDetach() { + TestComponent component = new TestComponent(); + Object constraints = null; + QuickSearch qs = QuickSearch.attach(component, constraints, new DummyCallback()); + assertEquals("One added key listener is expected after attach", 1, component.addedKeyListeners.size()); + assertTrue(qs.isEnabled()); + //assertFalse(qs.isAsynchronous()); + qs.detach(); + assertEquals("No key listener is expected after detach", 0, component.addedKeyListeners.size()); + } + + /** + * Test of isEnabled and setEnabled methods, of class QuickSearch. + */ + @Test + public void testIsEnabled() { + TestComponent component = new TestComponent(); + Object constraints = null; + QuickSearch qs = QuickSearch.attach(component, constraints, new DummyCallback()); + assertTrue(qs.isEnabled()); + qs.setEnabled(false); + assertEquals("No key listener is expected after setEnabled(false)", 0, component.addedKeyListeners.size()); + assertFalse(qs.isEnabled()); + qs.setEnabled(true); + assertTrue(qs.isEnabled()); + assertEquals("One added key listener is expected after setEnabled(true)", 1, component.addedKeyListeners.size()); + qs.detach(); + } + + /** + * Test of the addition of quick search component. + */ + @Test + public void testQuickSearchAdd() { + if (!SwingUtilities.isEventDispatchThread()) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + testQuickSearchAdd(); + } + }); + } catch (InterruptedException iex) { + fail("interrupted."); + } catch (InvocationTargetException itex) { + Throwable cause = itex.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + itex.getCause().printStackTrace(); + throw new AssertionError(cause); + } + return; + } + TestComponent component = new TestComponent(); + Object constraints = null; + QuickSearch qs = QuickSearch.attach(component, constraints, new DummyCallback()); + component.addNotify(); + KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A'); + //KeyboardFocusManager.getCurrentKeyboardFocusManager().setGlobalFocusOwner(component); + try { + Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class); + setGlobalFocusOwner.setAccessible(true); + setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), component); + } catch (Exception ex) { + ex.printStackTrace(); + throw new AssertionError(ex); + } + component.dispatchEvent(ke); + assertNotNull(component.added); + assertNull(component.constraints); + qs.detach(); + assertNull(component.added); + + constraints = new Object(); + qs = QuickSearch.attach(component, constraints, new DummyCallback()); + ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A'); + component.dispatchEvent(ke); + assertNotNull(component.added); + assertEquals(constraints, component.constraints); + qs.detach(); + assertNull(component.added); + } + + /** + * Test of the quick search listener. + */ + @Test + public void testQuickSearchListener() { + if (!SwingUtilities.isEventDispatchThread()) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + testQuickSearchListener(); + } + }); + } catch (InterruptedException iex) { + fail("interrupted."); + } catch (InvocationTargetException itex) { + Throwable cause = itex.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + itex.getCause().printStackTrace(); + throw new AssertionError(cause); + } + return; + } + TestComponent component = new TestComponent(); + Object constraints = null; + final String[] searchTextPtr = new String[] { null }; + final Boolean[] biasPtr = new Boolean[] { null }; + final boolean[] confirmedPtr = new boolean[] { false }; + final boolean[] canceledPtr = new boolean[] { false }; + QuickSearch.Callback qsc = new QuickSearch.Callback() { + + @Override + public boolean asynchronous() { + return false; + } + + @Override + public void quickSearchUpdate(String searchText) { + assertTrue(SwingUtilities.isEventDispatchThread()); + searchTextPtr[0] = searchText; + } + + @Override + public void showNextSelection(boolean forward) { + assertTrue(SwingUtilities.isEventDispatchThread()); + biasPtr[0] = forward; + } + + @Override + public String findMaxPrefix(String prefix) { + assertTrue(SwingUtilities.isEventDispatchThread()); + return prefix + "endPrefix"; + } + + @Override + public void quickSearchConfirmed() { + assertTrue(SwingUtilities.isEventDispatchThread()); + confirmedPtr[0] = true; + } + + @Override + public void quickSearchCanceled() { + assertTrue(SwingUtilities.isEventDispatchThread()); + canceledPtr[0] = true; + } + + }; + QuickSearch qs = QuickSearch.attach(component, constraints, qsc); + component.addNotify(); + // Test that a key event passed to the component triggers the quick search: + try { + Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class); + setGlobalFocusOwner.setAccessible(true); + setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), component); + } catch (Exception ex) { + ex.printStackTrace(); + throw new AssertionError(ex); + } + KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A'); + component.dispatchEvent(ke); + assertEquals("A", qs.getSearchField().getText()); + assertEquals("A", searchTextPtr[0]); + assertNull(biasPtr[0]); + + // Test that further key events passed to the quick search field trigger the quick search listener: + qs.getSearchField().setCaretPosition(1); + try { + Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class); + setGlobalFocusOwner.setAccessible(true); + setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), qs.getSearchField()); + } catch (Exception ex) { + ex.printStackTrace(); + throw new AssertionError(ex); + } + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'b'); + qs.getSearchField().dispatchEvent(ke); + assertEquals("Ab", searchTextPtr[0]); + + // Test the up/down keys resulting to selection navigation: + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_UP, (char) KeyEvent.VK_UP); + qs.getSearchField().dispatchEvent(ke); + assertEquals(Boolean.FALSE, biasPtr[0]); + + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_DOWN, (char) KeyEvent.VK_DOWN); + qs.getSearchField().dispatchEvent(ke); + assertEquals(Boolean.TRUE, biasPtr[0]); + + // Test that tab adds max prefix: + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_TAB, '\t'); + qs.getSearchField().dispatchEvent(ke); + assertEquals("AbendPrefix", qs.getSearchField().getText()); + + /* + // Test that we get no events when quick search listener is detached: + qs.removeQuickSearchListener(qsl); + qs.getSearchField().setCaretPosition(2); + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'c'); + qs.getSearchField().dispatchEvent(ke); + assertEquals("AbcendPrefix", qs.getSearchField().getText()); + assertEquals("Ab", searchTextPtr[0]); + qs.addQuickSearchListener(qsl); + */ + + // Test the quick search confirmation on Enter key: + assertFalse(confirmedPtr[0]); + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ENTER, '\n'); + qs.getSearchField().dispatchEvent(ke); + assertTrue(confirmedPtr[0]); + + // Test the quick search cancel on ESC key: + ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A'); + component.dispatchEvent(ke); + assertEquals("A", searchTextPtr[0]); + assertFalse(canceledPtr[0]); + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ESCAPE, (char) 27); + qs.getSearchField().dispatchEvent(ke); + assertTrue(canceledPtr[0]); + } + + enum sync { W, N } // Wait, Notify + + /** + * Test of asynchronous calls, of class QuickSearch. + */ + @Test + public void testAsynchronous() { + final TestComponent[] componentPtr = new TestComponent[] { null }; + final String[] searchTextPtr = new String[] { null }; + final Boolean[] biasPtr = new Boolean[] { null }; + final Object findMaxPrefixLock = new Object(); + final boolean[] confirmedPtr = new boolean[] { false }; + final boolean[] canceledPtr = new boolean[] { false }; + final boolean[] asynchronousPtr = new boolean[] { false }; + final sync[] syncPtr = new sync[] { null }; + final QuickSearch.Callback qsc = new QuickSearch.Callback() { + + @Override + public boolean asynchronous() { + return asynchronousPtr[0]; + } + + @Override + public void quickSearchUpdate(String searchText) { + assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread()); + synchronized(searchTextPtr) { + if (syncPtr[0] == null) { + syncPtr[0] = sync.W; + // Wait for the notification first + try { searchTextPtr.wait(); } catch (InterruptedException iex) {} + } + searchTextPtr[0] = searchText; + searchTextPtr.notifyAll(); + syncPtr[0] = null; + } + } + + @Override + public void showNextSelection(boolean forward) { + assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread()); + synchronized(biasPtr) { + if (syncPtr[0] == null) { + syncPtr[0] = sync.W; + // Wait for the notification first + try { biasPtr.wait(); } catch (InterruptedException iex) {} + } + biasPtr[0] = forward; + biasPtr.notifyAll(); + syncPtr[0] = null; + } + } + + @Override + public String findMaxPrefix(String prefix) { + assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread()); + synchronized(findMaxPrefixLock) { + if (syncPtr[0] == null) { + syncPtr[0] = sync.W; + // Wait for the notification first + try { findMaxPrefixLock.wait(); } catch (InterruptedException iex) {} + } + prefix = prefix + "endPrefix"; + findMaxPrefixLock.notifyAll(); + syncPtr[0] = null; + } + return prefix; + } + + @Override + public void quickSearchConfirmed() { + assertTrue(SwingUtilities.isEventDispatchThread()); + confirmedPtr[0] = true; + } + + @Override + public void quickSearchCanceled() { + assertTrue(SwingUtilities.isEventDispatchThread()); + canceledPtr[0] = true; + } + + }; + final QuickSearch[] qsPtr = new QuickSearch[] { null }; + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + componentPtr[0] = new TestComponent(); + qsPtr[0] = QuickSearch.attach(componentPtr[0], null, qsc); + componentPtr[0].addNotify(); + } + }); + } catch (InterruptedException iex) { + fail("interrupted."); + } catch (InvocationTargetException itex) { + Throwable cause = itex.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + itex.getCause().printStackTrace(); + throw new AssertionError(cause); + } + assertFalse(qsc.asynchronous()); + asynchronousPtr[0] = true; + assertTrue(qsc.asynchronous()); + + // Test that a key event passed to the component triggers the asynchronous quick search: + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + try { + Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class); + setGlobalFocusOwner.setAccessible(true); + setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), componentPtr[0]); + } catch (Exception ex) { + ex.printStackTrace(); + throw new AssertionError(ex); + } + KeyEvent ke = new KeyEvent(componentPtr[0], KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A'); + componentPtr[0].dispatchEvent(ke); + } + }); + } catch (InterruptedException iex) { + fail("interrupted."); + } catch (InvocationTargetException itex) { + Throwable cause = itex.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + itex.getCause().printStackTrace(); + throw new AssertionError(cause); + } + synchronized(searchTextPtr) { + assertNull(searchTextPtr[0]); + syncPtr[0] = sync.N; + searchTextPtr.notifyAll(); + // Wait to set the value + try { searchTextPtr.wait(); } catch (InterruptedException iex) {} + assertEquals("A", searchTextPtr[0]); + } + + // Test the up/down keys resulting to asynchronous selection navigation: + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + KeyEvent ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_UP, (char) KeyEvent.VK_UP); + qsPtr[0].getSearchField().dispatchEvent(ke); + + ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_DOWN, (char) KeyEvent.VK_DOWN); + qsPtr[0].getSearchField().dispatchEvent(ke); + } + }); + } catch (InterruptedException iex) { + fail("interrupted."); + } catch (InvocationTargetException itex) { + Throwable cause = itex.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + itex.getCause().printStackTrace(); + throw new AssertionError(cause); + } + synchronized(biasPtr) { + assertNull(biasPtr[0]); + syncPtr[0] = sync.N; + biasPtr.notifyAll(); + // Wait to set the value + try { biasPtr.wait(); } catch (InterruptedException iex) {} + assertEquals(Boolean.FALSE, biasPtr[0]); + } + synchronized(biasPtr) { + assertEquals(Boolean.FALSE, biasPtr[0]); + syncPtr[0] = sync.N; + biasPtr.notifyAll(); + // Wait to set the value + try { biasPtr.wait(); } catch (InterruptedException iex) {} + assertEquals(Boolean.TRUE, biasPtr[0]); + } + + // Test that tab adds max prefix asynchronously: + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + KeyEvent ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_TAB, '\t'); + qsPtr[0].getSearchField().dispatchEvent(ke); + } + }); + } catch (InterruptedException iex) { + fail("interrupted."); + } catch (InvocationTargetException itex) { + Throwable cause = itex.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + itex.getCause().printStackTrace(); + throw new AssertionError(cause); + } + synchronized(findMaxPrefixLock) { + assertEquals("A", qsPtr[0].getSearchField().getText()); + syncPtr[0] = sync.N; + findMaxPrefixLock.notifyAll(); + // Wait to set the value + try { findMaxPrefixLock.wait(); } catch (InterruptedException iex) {} + // Can not test it immediatelly, the text is updated in AWT + // assertEquals("AendPrefix", qsPtr[0].getSearchField().getText()); + } + try { Thread.sleep(200); } catch (InterruptedException iex) {} + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + assertEquals("AendPrefix", qsPtr[0].getSearchField().getText()); + } + }); + } catch (InterruptedException iex) { + fail("interrupted."); + } catch (InvocationTargetException itex) { + Throwable cause = itex.getCause(); + if (cause instanceof AssertionError) { + throw (AssertionError) cause; + } + itex.getCause().printStackTrace(); + throw new AssertionError(cause); + } + } + + /** + * Test of processKeyEvent method, of class QuickSearch. + */ + @Test + public void testProcessKeyEvent() { + TestComponent component = new TestComponent(); + Object constraints = null; + final String[] searchTextPtr = new String[] { null }; + final Boolean[] biasPtr = new Boolean[] { null }; + final boolean[] confirmedPtr = new boolean[] { false }; + final boolean[] canceledPtr = new boolean[] { false }; + final QuickSearch.Callback qsc = new QuickSearch.Callback() { + + @Override + public boolean asynchronous() { + return false; + } + + @Override + public void quickSearchUpdate(String searchText) { + searchTextPtr[0] = searchText; + } + + @Override + public void showNextSelection(boolean forward) { + biasPtr[0] = forward; + } + + @Override + public String findMaxPrefix(String prefix) { + return prefix + "endPrefix"; + } + + @Override + public void quickSearchConfirmed() { + confirmedPtr[0] = true; + } + + @Override + public void quickSearchCanceled() { + canceledPtr[0] = true; + } + }; + QuickSearch qs = QuickSearch.attach(component, constraints, qsc); + KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A'); + qs.processKeyEvent(ke); + assertEquals("A", qs.getSearchField().getText()); + assertEquals("A", searchTextPtr[0]); + assertNull(biasPtr[0]); + + ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'b'); + qs.processKeyEvent(ke); + assertEquals("Ab", qs.getSearchField().getText()); + assertEquals("Ab", searchTextPtr[0]); + } + + /** + * Test of findMaxCommonSubstring method, of class QuickSearch. + */ + @Test + public void testFindMaxCommonSubstring() { + System.out.println("findMaxCommonSubstring"); + String str1 = "annotation"; + String str2 = "antenna"; + boolean ignoreCase = false; + String expResult = "an"; + String result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase); + assertEquals(expResult, result); + str1 = "Annotation"; + expResult = ""; + result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase); + assertEquals(expResult, result); + str1 = "AbCdEf"; + str2 = "AbCxxx"; + expResult = "AbC"; + result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase); + assertEquals(expResult, result); + } + + private static final class TestComponent extends JComponent { + + List addedKeyListeners = new ArrayList(); + Component added; + Object constraints; + + public TestComponent() { + new JFrame().add(this); // To have a parent + } + + @Override + public boolean isShowing() { + return true; + } + + @Override + public Component add(Component comp) { + this.added = comp; + return super.add(comp); + } + + @Override + public void add(Component comp, Object constraints) { + this.added = comp; + this.constraints = constraints; + super.add(comp, constraints); + } + + @Override + public void remove(Component comp) { + if (comp == this.added) { + this.added = null; + } + super.remove(comp); + } + + @Override + public synchronized void addKeyListener(KeyListener l) { + addedKeyListeners.add(l); + super.addKeyListener(l); + } + + @Override + public synchronized void removeKeyListener(KeyListener l) { + addedKeyListeners.remove(l); + super.removeKeyListener(l); + } + + } + + private static final class DummyCallback implements QuickSearch.Callback { + + @Override + public boolean asynchronous() { + return false; + } + + @Override + public void quickSearchUpdate(String searchText) {} + + @Override + public void showNextSelection(boolean forward) {} + + @Override + public String findMaxPrefix(String prefix) { + return prefix; + } + + @Override + public void quickSearchConfirmed() {} + + @Override + public void quickSearchCanceled() {} + + } +}