Actions.forID
true
, three notification methods are called asynchronously
+ * on a background thread. These are
+ * {@link Callback#quickSearchUpdate(java.lang.String)},
+ * {@link Callback#showNextSelection(javax.swing.text.Position.Bias)},
+ * {@link Callback#findMaxPrefix(java.lang.String)}.
+ * If false
all methods are called synchronously on EQ thread.
+ * @param popupMenu A pop-up menu, that is displayed on the find icon, next to the search
+ * field. This allows customization of the search criteria. The pop-up menu
+ * is taken from {@link JMenu#getPopupMenu()}.
+ * @return An instance of QuickSearch class.
+ */
+ public static QuickSearch attach(JComponent component, Object constraints,
+ Callback callback, boolean asynchronous, JMenu popupMenu) {
+ Object qso = component.getClientProperty(CLIENT_PROPERTY_KEY);
+ if (qso instanceof QuickSearch) {
+ throw new IllegalStateException("A quick search is attached to this component already, detach it first."); // NOI18N
+ } else {
+ QuickSearch qs = new QuickSearch(component, constraints, callback, asynchronous, popupMenu);
+ component.putClientProperty(CLIENT_PROPERTY_KEY, qs);
+ return qs;
+ }
+ }
+
+ /**
+ * Detach the quick search from the component it was attached to.
+ */
+ public void detach() {
+ setEnabled(false);
+ component.putClientProperty(CLIENT_PROPERTY_KEY, null);
+ }
+
+ /**
+ * Test whether the quick search is enabled. This is true
+ * by default.
+ * @return true
when the quick search is enabled,
+ * false
otherwise.
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Set the enabled state of the quick search.
+ * This allows to activate/deactivate the quick search functionality.
+ * @param enabled true
to enable the quick search,
+ * false
otherwise.
+ */
+ public void setEnabled(boolean enabled) {
+ if (this.enabled == enabled) {
+ return ;
+ }
+ this.enabled = enabled;
+ if (enabled) {
+ component.addKeyListener(quickSearchKeyAdapter);
+ } else {
+ removeSearchField();
+ component.removeKeyListener(quickSearchKeyAdapter);
+ }
+ }
+
+ /**
+ * Process this key event in addition to the key events obtained from the
+ * component we're attached to.
+ * @param ke a key event to process.
+ */
+ public void processKeyEvent(KeyEvent ke) {
+ if (searchPanel != null) {
+ searchTextField.setCaretPosition(searchTextField.getText().length());
+ searchTextField.processKeyEvent(ke);
+ } else {
+ switch(ke.getID()) {
+ case KeyEvent.KEY_PRESSED:
+ quickSearchKeyAdapter.keyPressed(ke);
+ break;
+ case KeyEvent.KEY_RELEASED:
+ quickSearchKeyAdapter.keyReleased(ke);
+ break;
+ case KeyEvent.KEY_TYPED:
+ quickSearchKeyAdapter.keyTyped(ke);
+ break;
+ }
+ }
+ }
+
+ private void fireQuickSearchUpdate(String searchText) {
+ if (asynchronous) {
+ rp.post(new LazyFire(QS_FIRE.UPDATE, searchText));
+ } else {
+ callback.quickSearchUpdate(searchText);
+ }
+ }
+
+ private void fireShowNextSelection(boolean forward) {
+ if (asynchronous) {
+ rp.post(new LazyFire(QS_FIRE.NEXT, forward));
+ } else {
+ callback.showNextSelection(forward);
+ }
+ }
+
+ private void findMaxPrefix(String prefix, DataContentHandlerFactory newPrefixSetter) {
+ if (asynchronous) {
+ rp.post(new LazyFire(QS_FIRE.MAX, prefix, newPrefixSetter));
+ } else {
+ prefix = callback.findMaxPrefix(prefix);
+ newPrefixSetter.createDataContentHandler(prefix);
+ }
+ }
+
+ private void setUpSearch() {
+ searchTextField = new SearchTextField();
+ // create new key listeners
+ quickSearchKeyAdapter = (
+ new KeyAdapter() {
+ @Override
+ public void keyTyped(KeyEvent e) {
+ int modifiers = e.getModifiers();
+ int keyCode = e.getKeyCode();
+ char c = e.getKeyChar();
+
+ //#43617 - don't eat + and -
+ //#98634 - and all its duplicates dont't react to space
+ if ((c == '+') || (c == '-') || (c==' ')) return; // NOI18N
+
+ if (((modifiers > 0) && (modifiers != KeyEvent.SHIFT_MASK)) || e.isActionKey()) {
+ return;
+ }
+
+ if (Character.isISOControl(c) ||
+ (keyCode == KeyEvent.VK_SHIFT) ||
+ (keyCode == KeyEvent.VK_ESCAPE)) return;
+
+ displaySearchField();
+
+ final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
+ searchTextField.setText(String.valueOf(stroke.getKeyChar()));
+
+ e.consume();
+ }
+ }
+ );
+ if (isEnabled()) {
+ component.addKeyListener(quickSearchKeyAdapter);
+ }
+ // Create a the "multi-event" listener for the text field. Instead of
+ // adding separate instances of each needed listener, we're using a
+ // class which implements them all. This approach is used in order
+ // to avoid the creation of 4 instances which takes some time
+ searchFieldListener = new SearchFieldListener();
+ searchTextField.addKeyListener(searchFieldListener);
+ searchTextField.addFocusListener(searchFieldListener);
+ searchTextField.getDocument().addDocumentListener(searchFieldListener);
+
+ }
+
+ private void displaySearchField() {
+ if (searchPanel != null || !isEnabled()) {
+ return;
+ }
+ searchTextField.setOriginalFocusOwner();
+ searchTextField.setFont(component.getFont());
+ searchPanel = new SearchPanel();
+ final JLabel lbl;
+ if (popupMenu != null) {
+ lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND_WITH_MENU, false));
+ lbl.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ if (e != null && !SwingUtilities.isLeftMouseButton(e)) {
+ return;
+ }
+ JPopupMenu pm = popupMenu.getPopupMenu();
+ pm.show(lbl, 0, lbl.getHeight() - 1);
+ }
+ });
+ } else {
+ lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND, false));
+ }
+ if (asynchronous) {
+ animationTimer = new AnimationTimer(lbl, lbl.getIcon());
+ } else {
+ animationTimer = null;
+ }
+ searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS));
+ searchPanel.add(lbl);
+ searchPanel.add(searchTextField);
+ lbl.setLabelFor(searchTextField);
+ searchTextField.setColumns(10);
+ searchTextField.setMaximumSize(searchTextField.getPreferredSize());
+ searchTextField.putClientProperty("JTextField.variant", "search"); //NOI18N
+ lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
+ if (constraints == null) {
+ component.add(searchPanel);
+ } else {
+ component.add(searchPanel, constraints);
+ }
+ component.invalidate();
+ component.revalidate();
+ component.repaint();
+ searchTextField.requestFocus();
+ }
+
+ private void removeSearchField() {
+ if (searchPanel == null) {
+ return;
+ }
+ if (animationTimer != null) {
+ animationTimer.stopProgressAnimation();
+ }
+ Component sp = searchPanel;
+ searchPanel = null;
+ component.remove(sp);
+ component.invalidate();
+ component.revalidate();
+ component.repaint();
+ }
+
+ /** Accessed from test. */
+ JTextField getSearchField() {
+ return searchTextField;
+ }
+
+ /**
+ * Utility method, that finds a greatest common prefix of two supplied
+ * strings.
+ *
+ * @param str1 The first string
+ * @param str2 The second string
+ * @param ignoreCase Whether to ignore case in the comparisons
+ * @return The greatest common prefix of the two strings.
+ */
+ public static String findMaxPrefix(String str1, String str2, boolean ignoreCase) {
+ int n1 = str1.length();
+ int n2 = str2.length();
+ int i = 0;
+ if (ignoreCase) {
+ for ( ; i < n1 && i < n2; i++) {
+ char c1 = Character.toUpperCase(str1.charAt(i));
+ char c2 = Character.toUpperCase(str2.charAt(i));
+ if (c1 != c2) {
+ break;
+ }
+ }
+ } else {
+ for ( ; i < n1 && i < n2; i++) {
+ char c1 = str1.charAt(i);
+ char c2 = str2.charAt(i);
+ if (c1 != c2) {
+ break;
+ }
+ }
+ }
+ return str1.substring(0, i);
+ }
+
+ private final static class AnimationTimer {
+
+ private final JLabel jLabel;
+ private final Icon findIcon;
+ private final Timer animationTimer;
+
+ public AnimationTimer(final JLabel jLabel, Icon findIcon) {
+ this.jLabel = jLabel;
+ this.findIcon = findIcon;
+ animationTimer = new Timer(100, new ActionListener() {
+
+ ImageIcon icons[];
+ int index = 0;
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (icons == null) {
+ icons = new ImageIcon[8];
+ for (int i = 0; i < 8; i++) {
+ icons[i] = ImageUtilities.loadImageIcon("org/openide/awt/resources/quicksearch/progress_" + i + ".png", false); //NOI18N
+ }
+ }
+ jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 6));
+ jLabel.setIcon(icons[index]);
+ //mac os x
+ jLabel.repaint();
+
+ index = (index + 1) % 8;
+ }
+ });
+ }
+
+ public void startProgressAnimation() {
+ if (animationTimer != null && !animationTimer.isRunning()) {
+ animationTimer.start();
+ }
+ }
+
+ public void stopProgressAnimation() {
+ if (animationTimer != null && animationTimer.isRunning()) {
+ animationTimer.stop();
+ jLabel.setIcon(findIcon);
+ jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
+ }
+ }
+
+ }
+
+ private class LazyFire implements Runnable {
+
+ private final QS_FIRE fire;
+ //private final QuickSearchListener[] qsls;
+ private final String searchText;
+ private final boolean forward;
+ private final DataContentHandlerFactory newPrefixSetter;
+
+ LazyFire(QS_FIRE fire, String searchText) {
+ this(fire, searchText, true, null);
+ }
+
+ LazyFire(QS_FIRE fire, boolean forward) {
+ this(fire, null, forward);
+ }
+
+ LazyFire(QS_FIRE fire, String searchText, boolean forward) {
+ this(fire, searchText, forward, null);
+ }
+
+ LazyFire(QS_FIRE fire, String searchText,
+ DataContentHandlerFactory newPrefixSetter) {
+ this(fire, searchText, true, newPrefixSetter);
+ }
+
+ LazyFire(QS_FIRE fire, String searchText, boolean forward,
+ DataContentHandlerFactory newPrefixSetter) {
+ this.fire = fire;
+ //this.qsls = qsls;
+ this.searchText = searchText;
+ this.forward = forward;
+ this.newPrefixSetter = newPrefixSetter;
+ animationTimer.startProgressAnimation();
+ }
+
+ @Override
+ public void run() {
+ try {
+ switch (fire) {
+ case UPDATE: callback.quickSearchUpdate(searchText);//fireQuickSearchUpdate(qsls, searchText);
+ break;
+ case NEXT: callback.showNextSelection(forward);//fireShowNextSelection(qsls, forward);
+ break;
+ case MAX: String mp = callback.findMaxPrefix(searchText);//String mp = findMaxPrefix(qsls, searchText);
+ newPrefixSetter.createDataContentHandler(mp);
+ break;
+ }
+ } finally {
+ animationTimer.stopProgressAnimation();
+ }
+ }
+ }
+
+ private static class SearchPanel extends JPanel {
+
+ public static final boolean isAquaLaF =
+ "Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N
+
+ public SearchPanel() {
+ if (isAquaLaF) {
+ setBorder(BorderFactory.createEmptyBorder(9,6,8,2));
+ } else {
+ setBorder(BorderFactory.createEmptyBorder(2,6,2,2));
+ }
+ setOpaque(true);
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ if (isAquaLaF && g instanceof Graphics2D) {
+ Graphics2D g2d = (Graphics2D) g;
+ g2d.setPaint(new GradientPaint(0, 0, UIManager.getColor("NbExplorerView.quicksearch.background.top"), //NOI18N
+ 0, getHeight(), UIManager.getColor("NbExplorerView.quicksearch.background.bottom")));//NOI18N
+ g2d.fillRect(0, 0, getWidth(), getHeight());
+ g2d.setColor(UIManager.getColor("NbExplorerView.quicksearch.border")); //NOI18N
+ g2d.drawLine(0, 0, getWidth(), 0);
+ } else {
+ super.paintComponent(g);
+ }
+ }
+ }
+
+ /** searchTextField manages focus because it handles VK_ESCAPE key */
+ private class SearchTextField extends JTextField {
+
+ private WeakReferencefalse
+ * it's called in EQ thread, otherwise, it's called in a background thread.
+ * The client should update the visual representation of the search results
+ * and then return.
+ * This method is called to initiate and update the search process.
+ * @param searchText The new text to search for.
+ */
+ void quickSearchUpdate(String searchText);
+
+ /**
+ * Called to select a next occurrence of the search result.
+ * When {@link #isAsynchronous()} is false
+ * it's called in EQ thread, otherwise, it's called in a background thread.
+ * The client should update the visual representation of the search results
+ * and then return.
+ * @param forward The direction of the next search result.
+ * true
for forward direction,
+ * false
for backward direction.
+ */
+ void showNextSelection(boolean forward);
+
+ /**
+ * Find the maximum prefix among the search results, that starts with the provided string.
+ * This method is called when user press TAB in the search field, to auto-complete
+ * the maximum prefix.
+ * When {@link #isAsynchronous()} is false
+ * it's called in EQ thread, otherwise, it's called in a background thread.
+ * Utility method {@link QuickSearch#findMaxPrefix(java.lang.String, java.lang.String, boolean)}
+ * can be used by the implementation.
+ * @param prefix The prefix to start with
+ * @return The maximum prefix.
+ */
+ String findMaxPrefix(String prefix);
+
+ /**
+ * Called when the quick search is confirmed by the user.
+ * This method is called in EQ thread always.
+ */
+ void quickSearchConfirmed();
+
+ /**
+ * Called when the quick search is canceled by the user.
+ * This method is called in EQ thread always.
+ */
+ void quickSearchCanceled();
+
+ }
+
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ 6bce66b50582 Wed Mar 07 14:34:53 2012 +0100
@@ -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