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 and QuickSearchListener are added. They 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,27 +39,36 @@ * * 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 final JComponent component; private final Object constraints; @@ -67,8 +76,13 @@ private final List listeners = new LinkedList(); private SearchTextField searchTextField; private KeyAdapter quickSearchKeyAdapter; + private SearchFieldListener searchFieldListener; private JPanel searchPanel; private JMenu popupMenu; + private boolean asynchronous; + private RequestProcessor rp; + private static enum QS_FIRE { UPDATE, NEXT, MAX } + private AnimationTimer animationTimer; private QuickSearch(JComponent component, Object constraints) { this.component = component; @@ -76,10 +90,21 @@ 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. + * @return An instance of QuickSearch class. + */ public static QuickSearch attach(JComponent component, Object constraints) { Object qso = component.getClientProperty(QuickSearch.class.getName()); 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); @@ -87,15 +112,30 @@ } } + /** + * Detach the quick search from the component it was attached to. + */ public void detach() { setEnabled(false); component.putClientProperty(QuickSearch.class.getName(), 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,62 +144,154 @@ if (enabled) { component.addKeyListener(quickSearchKeyAdapter); } else { + removeSearchField(); component.removeKeyListener(quickSearchKeyAdapter); } } + /** + * Test whether the quick search notifies {@link QuickSearchListener} + * asynchronously, or not. + * By default, QuickSearchListener is notified synchronously on EQ thread. + * If true, three methods of QuickSearchListener are notified asynchronously + * on a background thread. These are + * {@link QuickSearchListener#quickSearchUpdate(java.lang.String)}, + * {@link QuickSearchListener#showNextSelection(javax.swing.text.Position.Bias)}, + * {@link QuickSearchListener#findMaxPrefix(java.lang.String)}. + * + * @return false for synchronous notification, + * true for asynchronous. + */ + public boolean isAsynchronous() { + return asynchronous; + } + + /** + * Set the asynchronous mode for the quick search notifications. + * @param asynchronous false for synchronous notification, + * true for asynchronous. + * @see #isAsynchronous() + */ + public void setAsynchronous(boolean asynchronous) { + if (this.asynchronous == asynchronous) { + return ; + } + this.asynchronous = asynchronous; + if (asynchronous) { + rp = new RequestProcessor(QuickSearch.class); + } else { + rp = null; + } + } + + /** + * Attach a {@link QuickSearchListener}. + * Notifications from the quick search field submissions are send to this + * listener. + * @param qsl the QuickSearchListener to attach. + */ public void addQuickSearchListener(QuickSearchListener qsl) { synchronized (listeners) { listeners.add(qsl); } } + /** + * Remove a {@link QuickSearchListener}. + * @param qsl the QuickSearchListener to remove. + */ public void removeQuickSearchListener(QuickSearchListener qsl) { synchronized (listeners) { listeners.remove(qsl); } } + /** + * Get the list of {@link QuickSearchListener}s attached to this quick search. + * @return The list of quick search listeners. + */ + public QuickSearchListener[] getQuickSearchListeners() { + synchronized (listeners) { + return listeners.toArray(new QuickSearchListener[] {}); + } + } + + /** + * Set a pop-up menu, that is displayed on the find icon, next to the search + * field. This allows customization of the search criteria. + * @param popupMenu The provider of pop-up menu, which is taken from + * {@link JMenu#getPopupMenu()}. + */ public void setPopupMenu(JMenu popupMenu) { this.popupMenu = popupMenu; } + /** + * 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) { - 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; + 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 QuickSearchListener[] getQuickSearchListeners() { - QuickSearchListener[] qsls; - synchronized (listeners) { - qsls = listeners.toArray(new QuickSearchListener[] {}); + private void fireQuickSearchUpdate(String searchText) { + QuickSearchListener[] qsls = getQuickSearchListeners(); + if (asynchronous) { + rp.post(new LazyFire(QS_FIRE.UPDATE, qsls, searchText)); + } else { + fireQuickSearchUpdate(qsls, searchText); } - return qsls; } - private void fireQuickSearchUpdate(String searchText) { - for (QuickSearchListener qsl : getQuickSearchListeners()) { + private void fireQuickSearchUpdate(QuickSearchListener[] qsls, String searchText) { + for (QuickSearchListener qsl : qsls) { qsl.quickSearchUpdate(searchText); } } - private void fireShowNextSelection(Bias bias) { - for (QuickSearchListener qsl : getQuickSearchListeners()) { - qsl.showNextSelection(bias); + private void fireShowNextSelection(boolean forward) { + QuickSearchListener[] qsls = getQuickSearchListeners(); + if (asynchronous) { + rp.post(new LazyFire(QS_FIRE.NEXT, qsls, forward)); + } else { + fireShowNextSelection(qsls, forward); } } - private String findMaxPrefix(String prefix) { - for (QuickSearchListener qsl : getQuickSearchListeners()) { + private void fireShowNextSelection(QuickSearchListener[] qsls, boolean forward) { + for (QuickSearchListener qsl : qsls) { + qsl.showNextSelection(forward); + } + } + + private void findMaxPrefix(String prefix, DataContentHandlerFactory newPrefixSetter) { + QuickSearchListener[] qsls = getQuickSearchListeners(); + if (asynchronous) { + rp.post(new LazyFire(QS_FIRE.MAX, qsls, prefix, newPrefixSetter)); + } else { + prefix = findMaxPrefix(qsls, prefix); + newPrefixSetter.createDataContentHandler(prefix); + } + } + + private String findMaxPrefix(QuickSearchListener[] qsls, String prefix) { + for (QuickSearchListener qsl : qsls) { prefix = qsl.findMaxPrefix(prefix); } return prefix; @@ -200,22 +332,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 +359,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 +378,11 @@ } 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); @@ -263,12 +391,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 +406,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 +454,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, QuickSearchListener[] qsls, String searchText) { + this(fire, qsls, searchText, true, null); + } - void quickSearchConfirmed(); + LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, boolean forward) { + this(fire, qsls, null, forward); + } - void quickSearchCanceled(); + LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, String searchText, boolean forward) { + this(fire, qsls, searchText, forward, null); + } + + LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, String searchText, + DataContentHandlerFactory newPrefixSetter) { + this(fire, qsls, searchText, true, newPrefixSetter); + } + + LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, 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: fireQuickSearchUpdate(qsls, searchText); + break; + case NEXT: fireShowNextSelection(qsls, forward); + break; + case MAX: 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 +572,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 @@ -449,23 +678,37 @@ fireQuickSearchCanceled(); 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) { @@ -506,8 +749,10 @@ if (oppositeComponent == searchTextField) { return ; } - removeSearchField(); - fireQuickSearchConfirmed(); + if (searchPanel != null) { + removeSearchField(); + fireQuickSearchConfirmed(); + } } } diff --git a/openide.awt/src/org/openide/awt/QuickSearchListener.java b/openide.awt/src/org/openide/awt/QuickSearchListener.java new file mode 100644 --- /dev/null +++ b/openide.awt/src/org/openide/awt/QuickSearchListener.java @@ -0,0 +1,100 @@ +/* + * 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; + +/** + * Listener that is notified with the submissions to the quick search field. + * + * @author Martin Entlicher + * @since 7.43 + */ +public interface QuickSearchListener { + + /** + * Called with an updated search text. + * When {@link QuickSearch#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 QuickSearch#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 QuickSearch#isAsynchronous()} is false + * it's called in EQ thread, otherwise, it's called in a background thread. + * Utility method {@link QuickSearch#findMaxCommonSubstring(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,658 @@ +/* + * 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); + 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); + 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); + 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); + 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; + QuickSearch qs = QuickSearch.attach(component, constraints); + component.addNotify(); + final String[] searchTextPtr = new String[] { null }; + final Boolean[] biasPtr = new Boolean[] { null }; + final boolean[] confirmedPtr = new boolean[] { false }; + final boolean[] canceledPtr = new boolean[] { false }; + QuickSearchListener qsl = new QuickSearchListener() { + + @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; + } + }; + qs.addQuickSearchListener(qsl); + // 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]); + + // Test the quick search confirmation on Enter key: + qs.addQuickSearchListener(qsl); + 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 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); + 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); + } + 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 }; + QuickSearchListener qsl = new QuickSearchListener() { + + @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; + } + }; + qsPtr[0].addQuickSearchListener(qsl); + assertFalse(qsPtr[0].isAsynchronous()); + qsPtr[0].setAsynchronous(true); + asynchronousPtr[0] = true; + assertTrue(qsPtr[0].isAsynchronous()); + + // 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; + QuickSearch qs = QuickSearch.attach(component, constraints); + final String[] searchTextPtr = new String[] { null }; + final Boolean[] biasPtr = new Boolean[] { null }; + final boolean[] confirmedPtr = new boolean[] { false }; + final boolean[] canceledPtr = new boolean[] { false }; + QuickSearchListener qsl = new QuickSearchListener() { + + @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; + } + }; + qs.addQuickSearchListener(qsl); + 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); + } + + } +}