--- a/openide.awt/apichanges.xml Tue Mar 06 10:49:56 2012 +0100 +++ a/openide.awt/apichanges.xml Wed Mar 07 14:34:53 2012 +0100 @@ -50,6 +50,19 @@ 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 --- a/openide.awt/manifest.mf Tue Mar 06 10:49:56 2012 +0100 +++ a/openide.awt/manifest.mf Wed Mar 07 14:34:53 2012 +0100 @@ -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 --- a/openide.awt/nbproject/project.xml Tue Mar 06 10:49:56 2012 +0100 +++ a/openide.awt/nbproject/project.xml Wed Mar 07 14:34:53 2012 +0100 @@ -50,6 +50,15 @@ org.openide.awt + org.netbeans.api.annotations.common + + + + 1 + 1.13 + + + org.openide.filesystems --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ 6bce66b50582 Wed Mar 07 14:34:53 2012 +0100 @@ -0,0 +1,791 @@ +/* + * 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.*; +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 org.netbeans.api.annotations.common.StaticResource; +import org.openide.util.ImageUtilities; +import org.openide.util.RequestProcessor; + +/** + * Quick search infrastructure for an arbitrary component. + * When quick search is attached to a component, it listens on key events going + * to the component and displays a quick search field. + * + * @author Martin Entlicher + * @since 7.43 + */ +public class QuickSearch { + + @StaticResource + private static final String ICON_FIND = "org/openide/awt/resources/quicksearch/find.png"; // NOI18N + @StaticResource + private static final String ICON_FIND_WITH_MENU = "org/openide/awt/resources/quicksearch/findMenu.png"; // NOI18N + private static final Object CLIENT_PROPERTY_KEY = new Object(); + + private final JComponent component; + private final Object constraints; + private final Callback callback; + private final JMenu popupMenu; + private final boolean asynchronous; + private boolean enabled = true; + private SearchTextField searchTextField; + private KeyAdapter quickSearchKeyAdapter; + private SearchFieldListener searchFieldListener; + private JPanel searchPanel; + private final RequestProcessor rp; + private static enum QS_FIRE { UPDATE, NEXT, MAX } + private AnimationTimer animationTimer; + + private QuickSearch(JComponent component, Object constraints, + Callback callback, boolean asynchronous, JMenu popupMenu) { + this.component = component; + this.constraints = constraints; + this.callback = callback; + this.asynchronous = asynchronous; + this.popupMenu = popupMenu; + if (asynchronous) { + rp = new RequestProcessor(QuickSearch.class); + } else { + rp = null; + } + setUpSearch(); + } + + /** + * Attach quick search to a component with given constraints. + * It listens on key events going to the component and displays a quick search + * field. + * + * @param component The component to attach to + * @param constraints The constraints that are used to add the search field + * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)} + * when adding the quick search UI to the component. + * @param callback The call back implementation, which is notified from the + * quick search field submissions. + * @return An instance of QuickSearch class. + */ + public static QuickSearch attach(JComponent component, Object constraints, + Callback callback) { + return attach(component, constraints, callback, false, null); + } + + /** + * Attach quick search to a component with given constraints. + * It listens on key events going to the component and displays a quick search + * field. + * + * @param component The component to attach to + * @param constraints The constraints that are used to add the search field + * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)} + * when adding the quick search UI to the component. + * @param callback The call back implementation, which is notified from the + * quick search field submissions. + * @return An instance of QuickSearch class. + */ + public static QuickSearch attach(JComponent component, Object constraints, + Callback callback, boolean asynchronous) { + return attach(component, constraints, callback, asynchronous, null); + } + + /** + * Attach quick search to a component with given constraints. + * It listens on key events going to the component and displays a quick search + * field. + * + * @param component The component to attach to + * @param constraints The constraints that are used to add the search field + * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)} + * when adding the quick search UI to the component. + * @param callback The call back implementation, which is notified from the + * quick search field submissions. + * @param popupMenu A pop-up menu, that is displayed on the find icon, next to the search + * field. This allows customization of the search criteria. The pop-up menu + * is taken from {@link JMenu#getPopupMenu()}. + * @return An instance of QuickSearch class. + */ + public static QuickSearch attach(JComponent component, Object constraints, + Callback callback, JMenu popupMenu) { + return attach(component, constraints, callback, false, popupMenu); + } + /** + * Attach quick search to a component with given constraints. + * It listens on key events going to the component and displays a quick search + * field. + * + * @param component The component to attach to + * @param constraints The constraints that are used to add the search field + * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)} + * when adding the quick search UI to the component. + * @param callback The call back implementation, which is notified from the + * quick search field submissions. + * @param asynchronous Set whether the quick search notifies the call back + * asynchronously, or not. + * By default, Callback is notified synchronously on EQ thread. + * If true, three notification methods are called asynchronously + * on a background thread. These are + * {@link Callback#quickSearchUpdate(java.lang.String)}, + * {@link Callback#showNextSelection(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 WeakReference originalFocusOwner = new WeakReference(null); + + public SearchTextField() { + } + + void setOriginalFocusOwner() { + Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); + if (focusOwner != null && component.isAncestorOf(focusOwner)) { + originalFocusOwner = new WeakReference(focusOwner); + } else { + originalFocusOwner = new WeakReference(component); + } + } + + void requestOriginalFocusOwner() { + SwingUtilities.invokeLater( + new Runnable() { + //additional bugfix - do focus change later or removing + //the component while it's focused will cause focus to + //get transferred to the next component in the + //parent focusTraversalPolicy *after* our request + //focus completes, so focus goes into a black hole - Tim + @Override + public void run() { + Component fo = originalFocusOwner.get(); + if (fo != null) { + fo.requestFocusInWindow(); + } + } + } + ); + } + + @Override + public boolean isManagingFocus() { + return true; + } + + @Override + public void processKeyEvent(KeyEvent ke) { + //override the default handling so that + //the parent will never receive the escape key and + //close a modal dialog + if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) { + removeSearchField(); + ke.consume(); + // bugfix #32909, reqest focus when search field is removed + requestOriginalFocusOwner(); + //fireQuickSearchCanceled(); + callback.quickSearchCanceled(); + } else { + super.processKeyEvent(ke); + } + } + }; + + private class SearchFieldListener extends KeyAdapter implements DocumentListener, FocusListener { + + private boolean ignoreEvents; + + SearchFieldListener() { + } + + @Override + public void changedUpdate(DocumentEvent e) { + if (ignoreEvents) return; + searchForNode(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + if (ignoreEvents) return; + searchForNode(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + if (ignoreEvents) return; + searchForNode(); + } + + @Override + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + + if (keyCode == KeyEvent.VK_ESCAPE) { + removeSearchField(); + searchTextField.requestOriginalFocusOwner(); + //fireQuickSearchCanceled(); + callback.quickSearchCanceled(); + e.consume(); + } else if (keyCode == KeyEvent.VK_UP || (keyCode == KeyEvent.VK_F3 && e.isShiftDown())) { + fireShowNextSelection(false); + // Stop processing the event here. Otherwise it's dispatched + // to the tree too (which scrolls) + e.consume(); + } else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_F3) { + fireShowNextSelection(true); + // Stop processing the event here. Otherwise it's dispatched + // to the tree too (which scrolls) + e.consume(); + } else if (keyCode == KeyEvent.VK_TAB) { + findMaxPrefix(searchTextField.getText(), new 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(); + callback.quickSearchConfirmed(); + + component.requestFocusInWindow(); + e.consume(); + } + } + + /** Searches for a node in the tree. */ + private void searchForNode() { + String text = searchTextField.getText(); + fireQuickSearchUpdate(text); + } + + @Override + public void focusGained(FocusEvent e) { + if (e.getSource() == searchTextField) { + // make sure nothing is selected + int n = searchTextField.getText().length(); + searchTextField.select(n, n); + } + } + + @Override + public void focusLost(FocusEvent e) { + if (e.isTemporary()) return ; + Component oppositeComponent = e.getOppositeComponent(); + if (e.getSource() != searchTextField) { + ((Component) e.getSource()).removeFocusListener(this); + } + if (oppositeComponent instanceof JMenuItem || oppositeComponent instanceof JPopupMenu) { + oppositeComponent.addFocusListener(this); + return ; + } + if (oppositeComponent == searchTextField) { + return ; + } + if (searchPanel != null) { + removeSearchField(); + //fireQuickSearchConfirmed(); + callback.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 { + + /** + * 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(); + + } + +} --- /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 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() {} + + } +}