This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

View | Details | Raw Unified | Return to bug 208794
Collapse All | Expand All

(-)a/openide.awt/apichanges.xml (+14 lines)
Lines 50-55 Link Here
50
<apidef name="awt">AWT API</apidef>
50
<apidef name="awt">AWT API</apidef>
51
</apidefs>
51
</apidefs>
52
<changes>
52
<changes>
53
    <change id="QuickSearch">
54
      <api name="awt"/>
55
      <summary>QuickSearch class that allows to attach quick search field to an arbitratry component.</summary>
56
      <date day="29" month="2" year="2012"/>
57
      <author login="mentlicher"/>
58
      <compatibility addition="yes" binary="compatible" source="compatible" deprecation="no" semantic="compatible" modification="no" deletion="no"/>
59
      <description>
60
          QuickSearch class and QuickSearchListener are added. They can be used to attach
61
          a quick search functionality to an arbitrary component.
62
      </description>
63
      <class package="org.openide.awt" name="QuickSearch"/>
64
      <class package="org.openide.awt" name="QuickSearchListener"/>
65
      <issue number="208794"/>
66
    </change>
53
    <change id="Actions.forID">
67
    <change id="Actions.forID">
54
        <api name="awt"/>
68
        <api name="awt"/>
55
        <summary>Added <code>Actions.forID</code></summary>
69
        <summary>Added <code>Actions.forID</code></summary>
(-)a/openide.awt/manifest.mf (-1 / +1 lines)
Lines 2-6 Link Here
2
OpenIDE-Module: org.openide.awt
2
OpenIDE-Module: org.openide.awt
3
OpenIDE-Module-Localizing-Bundle: org/openide/awt/Bundle.properties
3
OpenIDE-Module-Localizing-Bundle: org/openide/awt/Bundle.properties
4
AutoUpdate-Essential-Module: true
4
AutoUpdate-Essential-Module: true
5
OpenIDE-Module-Specification-Version: 7.42
5
OpenIDE-Module-Specification-Version: 7.43
6
6
(-)a/openide.awt/nbproject/project.xml (+9 lines)
Lines 50-55 Link Here
50
            <code-name-base>org.openide.awt</code-name-base>
50
            <code-name-base>org.openide.awt</code-name-base>
51
            <module-dependencies>
51
            <module-dependencies>
52
                <dependency>
52
                <dependency>
53
                    <code-name-base>org.netbeans.api.annotations.common</code-name-base>
54
                    <build-prerequisite/>
55
                    <compile-dependency/>
56
                    <run-dependency>
57
                        <release-version>1</release-version>
58
                        <specification-version>1.13</specification-version>
59
                    </run-dependency>
60
                </dependency>
61
                <dependency>
53
                    <code-name-base>org.openide.filesystems</code-name-base>
62
                    <code-name-base>org.openide.filesystems</code-name-base>
54
                    <build-prerequisite/>
63
                    <build-prerequisite/>
55
                    <compile-dependency/>
64
                    <compile-dependency/>
(-)a/openide.explorer/src/org/openide/explorer/view/QuickSearch.java (-71 / +316 lines)
Lines 39-65 Link Here
39
 *
39
 *
40
 * Portions Copyrighted 2012 Sun Microsystems, Inc.
40
 * Portions Copyrighted 2012 Sun Microsystems, Inc.
41
 */
41
 */
42
package org.openide.explorer.view;
42
package org.openide.awt;
43
43
44
import java.awt.*;
44
import java.awt.*;
45
import java.awt.event.*;
45
import java.awt.event.*;
46
import java.lang.ref.WeakReference;
46
import java.lang.ref.WeakReference;
47
import java.util.LinkedList;
47
import java.util.LinkedList;
48
import java.util.List;
48
import java.util.List;
49
import javax.activation.DataContentHandler;
50
import javax.activation.DataContentHandlerFactory;
49
import javax.swing.*;
51
import javax.swing.*;
50
import javax.swing.event.DocumentEvent;
52
import javax.swing.event.DocumentEvent;
51
import javax.swing.event.DocumentListener;
53
import javax.swing.event.DocumentListener;
52
import javax.swing.text.Position.Bias;
54
import org.netbeans.api.annotations.common.StaticResource;
55
import org.openide.util.ImageUtilities;
56
import org.openide.util.RequestProcessor;
53
57
54
/**
58
/**
55
 * Quick search infrastructure
59
 * Quick search infrastructure for an arbitrary component.
60
 * When quick search is attached to a component, it listens on key events going
61
 * to the component and displays a quick search field.
56
 * 
62
 * 
57
 * @author Martin Entlicher
63
 * @author Martin Entlicher
64
 * @since 7.43
58
 */
65
 */
59
class QuickSearch {
66
public class QuickSearch {
60
    
67
    
61
    private static final String ICON_FIND = "org/openide/explorer/view/find.png";
68
    @StaticResource
62
    private static final String ICON_FIND_WITH_MENU = "org/openide/explorer/view/findMenu.png";
69
    private static final String ICON_FIND = "org/openide/awt/resources/quicksearch/find.png";      // NOI18N
70
    @StaticResource
71
    private static final String ICON_FIND_WITH_MENU = "org/openide/awt/resources/quicksearch/findMenu.png"; // NOI18N
63
    
72
    
64
    private final JComponent component;
73
    private final JComponent component;
65
    private final Object constraints;
74
    private final Object constraints;
Lines 67-74 Link Here
67
    private final List<QuickSearchListener> listeners = new LinkedList<QuickSearchListener>();
76
    private final List<QuickSearchListener> listeners = new LinkedList<QuickSearchListener>();
68
    private SearchTextField searchTextField;
77
    private SearchTextField searchTextField;
69
    private KeyAdapter quickSearchKeyAdapter;
78
    private KeyAdapter quickSearchKeyAdapter;
79
    private SearchFieldListener searchFieldListener;
70
    private JPanel searchPanel;
80
    private JPanel searchPanel;
71
    private JMenu popupMenu;
81
    private JMenu popupMenu;
82
    private boolean asynchronous;
83
    private RequestProcessor rp;
84
    private static enum QS_FIRE { UPDATE, NEXT, MAX }
85
    private AnimationTimer animationTimer;
72
    
86
    
73
    private QuickSearch(JComponent component, Object constraints) {
87
    private QuickSearch(JComponent component, Object constraints) {
74
        this.component = component;
88
        this.component = component;
Lines 76-85 Link Here
76
        setUpSearch();
90
        setUpSearch();
77
    }
91
    }
78
    
92
    
93
    /**
94
     * Attach quick search to a component with given constraints.
95
     * It listens on key events going to the component and displays a quick search
96
     * field.
97
     * 
98
     * @param component The component to attach to
99
     * @param constraints The constraints that are used to add the search field
100
     * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
101
     * when adding the quick search UI to the component.
102
     * @return An instance of QuickSearch class.
103
     */
79
    public static QuickSearch attach(JComponent component, Object constraints) {
104
    public static QuickSearch attach(JComponent component, Object constraints) {
80
        Object qso = component.getClientProperty(QuickSearch.class.getName());
105
        Object qso = component.getClientProperty(QuickSearch.class.getName());
81
        if (qso instanceof QuickSearch) {
106
        if (qso instanceof QuickSearch) {
82
            return (QuickSearch) qso;
107
            throw new IllegalStateException("A quick search is attached to this component already, detach it first."); // NOI18N
83
        } else {
108
        } else {
84
            QuickSearch qs = new QuickSearch(component, constraints);
109
            QuickSearch qs = new QuickSearch(component, constraints);
85
            component.putClientProperty(QuickSearch.class.getName(), qs);
110
            component.putClientProperty(QuickSearch.class.getName(), qs);
Lines 87-101 Link Here
87
        }
112
        }
88
    }
113
    }
89
    
114
    
115
    /**
116
     * Detach the quick search from the component it was attached to.
117
     */
90
    public void detach() {
118
    public void detach() {
91
        setEnabled(false);
119
        setEnabled(false);
92
        component.putClientProperty(QuickSearch.class.getName(), null);
120
        component.putClientProperty(QuickSearch.class.getName(), null);
93
    }
121
    }
94
    
122
    
123
    /**
124
     * Test whether the quick search is enabled. This is <code>true</code>
125
     * by default.
126
     * @return <code>true</code> when the quick search is enabled,
127
     *         <code>false</code> otherwise.
128
     */
95
    public boolean isEnabled() {
129
    public boolean isEnabled() {
96
        return enabled;
130
        return enabled;
97
    }
131
    }
98
    
132
    
133
    /**
134
     * Set the enabled state of the quick search.
135
     * This allows to activate/deactivate the quick search functionality.
136
     * @param enabled <code>true</code> to enable the quick search,
137
     *                <code>false</code> otherwise.
138
     */
99
    public void setEnabled(boolean enabled) {
139
    public void setEnabled(boolean enabled) {
100
        if (this.enabled == enabled) {
140
        if (this.enabled == enabled) {
101
            return ;
141
            return ;
Lines 104-165 Link Here
104
        if (enabled) {
144
        if (enabled) {
105
            component.addKeyListener(quickSearchKeyAdapter);
145
            component.addKeyListener(quickSearchKeyAdapter);
106
        } else {
146
        } else {
147
            removeSearchField();
107
            component.removeKeyListener(quickSearchKeyAdapter);
148
            component.removeKeyListener(quickSearchKeyAdapter);
108
        }
149
        }
109
    }
150
    }
110
    
151
    
152
    /**
153
     * Test whether the quick search notifies {@link QuickSearchListener}
154
     * asynchronously, or not.
155
     * By default, QuickSearchListener is notified synchronously on EQ thread.
156
     * If <code>true</code>, three methods of QuickSearchListener are notified asynchronously
157
     * on a background thread. These are
158
     * {@link QuickSearchListener#quickSearchUpdate(java.lang.String)},
159
     * {@link QuickSearchListener#showNextSelection(javax.swing.text.Position.Bias)},
160
     * {@link QuickSearchListener#findMaxPrefix(java.lang.String)}.
161
     * 
162
     * @return <code>false</code> for synchronous notification,
163
     *         <code>true</code> for asynchronous.
164
     */
165
    public boolean isAsynchronous() {
166
        return asynchronous;
167
    }
168
    
169
    /**
170
     * Set the asynchronous mode for the quick search notifications.
171
     * @param asynchronous <code>false</code> for synchronous notification,
172
     *                     <code>true</code> for asynchronous.
173
     * @see #isAsynchronous()
174
     */
175
    public void setAsynchronous(boolean asynchronous) {
176
        if (this.asynchronous == asynchronous) {
177
            return ;
178
        }
179
        this.asynchronous = asynchronous;
180
        if (asynchronous) {
181
            rp = new RequestProcessor(QuickSearch.class);
182
        } else {
183
            rp = null;
184
        }
185
    }
186
    
187
    /**
188
     * Attach a {@link QuickSearchListener}.
189
     * Notifications from the quick search field submissions are send to this
190
     * listener.
191
     * @param qsl the QuickSearchListener to attach.
192
     */
111
    public void addQuickSearchListener(QuickSearchListener qsl) {
193
    public void addQuickSearchListener(QuickSearchListener qsl) {
112
        synchronized (listeners) {
194
        synchronized (listeners) {
113
            listeners.add(qsl);
195
            listeners.add(qsl);
114
        }
196
        }
115
    }
197
    }
116
    
198
    
199
    /**
200
     * Remove a {@link QuickSearchListener}.
201
     * @param qsl the QuickSearchListener to remove.
202
     */
117
    public void removeQuickSearchListener(QuickSearchListener qsl) {
203
    public void removeQuickSearchListener(QuickSearchListener qsl) {
118
        synchronized (listeners) {
204
        synchronized (listeners) {
119
            listeners.remove(qsl);
205
            listeners.remove(qsl);
120
        }
206
        }
121
    }
207
    }
122
    
208
    
209
    /**
210
     * Get the list of {@link QuickSearchListener}s attached to this quick search.
211
     * @return The list of quick search listeners.
212
     */
213
    public QuickSearchListener[] getQuickSearchListeners() {
214
        synchronized (listeners) {
215
            return listeners.toArray(new QuickSearchListener[] {});
216
        }
217
    }
218
    
219
    /**
220
     * Set a pop-up menu, that is displayed on the find icon, next to the search
221
     * field. This allows customization of the search criteria.
222
     * @param popupMenu The provider of pop-up menu, which is taken from
223
     *                  {@link JMenu#getPopupMenu()}.
224
     */
123
    public void setPopupMenu(JMenu popupMenu) {
225
    public void setPopupMenu(JMenu popupMenu) {
124
        this.popupMenu = popupMenu;
226
        this.popupMenu = popupMenu;
125
    }
227
    }
126
    
228
    
229
    /**
230
     * Process this key event in addition to the key events obtained from the
231
     * component we're attached to.
232
     * @param ke a key event to process.
233
     */
127
    public void processKeyEvent(KeyEvent ke) {
234
    public void processKeyEvent(KeyEvent ke) {
128
        switch(ke.getID()) {
235
        if (searchPanel != null) {
129
            case KeyEvent.KEY_PRESSED:
236
            searchTextField.setCaretPosition(searchTextField.getText().length());
130
                quickSearchKeyAdapter.keyPressed(ke);
237
            searchTextField.processKeyEvent(ke);
131
                break;
238
        } else {
132
            case KeyEvent.KEY_RELEASED:
239
            switch(ke.getID()) {
133
                quickSearchKeyAdapter.keyReleased(ke);
240
                case KeyEvent.KEY_PRESSED:
134
                break;
241
                    quickSearchKeyAdapter.keyPressed(ke);
135
            case KeyEvent.KEY_TYPED:
242
                    break;
136
                quickSearchKeyAdapter.keyTyped(ke);
243
                case KeyEvent.KEY_RELEASED:
137
                break;
244
                    quickSearchKeyAdapter.keyReleased(ke);
245
                    break;
246
                case KeyEvent.KEY_TYPED:
247
                    quickSearchKeyAdapter.keyTyped(ke);
248
                    break;
249
            }
138
        }
250
        }
139
    }
251
    }
140
    
252
    
141
    private QuickSearchListener[] getQuickSearchListeners() {
253
    private void fireQuickSearchUpdate(String searchText) {
142
        QuickSearchListener[] qsls;
254
        QuickSearchListener[] qsls = getQuickSearchListeners();
143
        synchronized (listeners) {
255
        if (asynchronous) {
144
            qsls = listeners.toArray(new QuickSearchListener[] {});
256
            rp.post(new LazyFire(QS_FIRE.UPDATE, qsls, searchText));
257
        } else {
258
            fireQuickSearchUpdate(qsls, searchText);
145
        }
259
        }
146
        return qsls;
147
    }
260
    }
148
    
261
    
149
    private void fireQuickSearchUpdate(String searchText) {
262
    private void fireQuickSearchUpdate(QuickSearchListener[] qsls, String searchText) {
150
        for (QuickSearchListener qsl : getQuickSearchListeners()) {
263
        for (QuickSearchListener qsl : qsls) {
151
            qsl.quickSearchUpdate(searchText);
264
            qsl.quickSearchUpdate(searchText);
152
        }
265
        }
153
    }
266
    }
154
    
267
    
155
    private void fireShowNextSelection(Bias bias) {
268
    private void fireShowNextSelection(boolean forward) {
156
        for (QuickSearchListener qsl : getQuickSearchListeners()) {
269
        QuickSearchListener[] qsls = getQuickSearchListeners();
157
            qsl.showNextSelection(bias);
270
        if (asynchronous) {
271
            rp.post(new LazyFire(QS_FIRE.NEXT, qsls, forward));
272
        } else {
273
            fireShowNextSelection(qsls, forward);
158
        }
274
        }
159
    }
275
    }
160
    
276
    
161
    private String findMaxPrefix(String prefix) {
277
    private void fireShowNextSelection(QuickSearchListener[] qsls, boolean forward) {
162
        for (QuickSearchListener qsl : getQuickSearchListeners()) {
278
        for (QuickSearchListener qsl : qsls) {
279
            qsl.showNextSelection(forward);
280
        }
281
    }
282
    
283
    private void findMaxPrefix(String prefix, DataContentHandlerFactory newPrefixSetter) {
284
        QuickSearchListener[] qsls = getQuickSearchListeners();
285
        if (asynchronous) {
286
            rp.post(new LazyFire(QS_FIRE.MAX, qsls, prefix, newPrefixSetter));
287
        } else {
288
            prefix = findMaxPrefix(qsls, prefix);
289
            newPrefixSetter.createDataContentHandler(prefix);
290
        }
291
    }
292
    
293
    private String findMaxPrefix(QuickSearchListener[] qsls, String prefix) {
294
        for (QuickSearchListener qsl : qsls) {
163
            prefix = qsl.findMaxPrefix(prefix);
295
            prefix = qsl.findMaxPrefix(prefix);
164
        }
296
        }
165
        return prefix;
297
        return prefix;
Lines 200-221 Link Here
200
                            (keyCode == KeyEvent.VK_SHIFT) ||
332
                            (keyCode == KeyEvent.VK_SHIFT) ||
201
                            (keyCode == KeyEvent.VK_ESCAPE)) return;
333
                            (keyCode == KeyEvent.VK_ESCAPE)) return;
202
334
335
                    displaySearchField();
336
                    
203
                    final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
337
                    final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
204
                    searchTextField.setText(String.valueOf(stroke.getKeyChar()));
338
                    searchTextField.setText(String.valueOf(stroke.getKeyChar()));
205
339
206
                    displaySearchField();
207
                    e.consume();
340
                    e.consume();
208
                }
341
                }
209
            }
342
            }
210
        );
343
        );
211
        if(isEnabled()){
344
        if (isEnabled()) {
212
            component.addKeyListener(quickSearchKeyAdapter);
345
            component.addKeyListener(quickSearchKeyAdapter);
213
        }
346
        }
214
        // Create a the "multi-event" listener for the text field. Instead of
347
        // Create a the "multi-event" listener for the text field. Instead of
215
        // adding separate instances of each needed listener, we're using a
348
        // adding separate instances of each needed listener, we're using a
216
        // class which implements them all. This approach is used in order 
349
        // class which implements them all. This approach is used in order 
217
        // to avoid the creation of 4 instances which takes some time
350
        // to avoid the creation of 4 instances which takes some time
218
        SearchFieldListener searchFieldListener = new SearchFieldListener();
351
        searchFieldListener = new SearchFieldListener();
219
        searchTextField.addKeyListener(searchFieldListener);
352
        searchTextField.addKeyListener(searchFieldListener);
220
        searchTextField.addFocusListener(searchFieldListener);
353
        searchTextField.addFocusListener(searchFieldListener);
221
        searchTextField.getDocument().addDocumentListener(searchFieldListener);
354
        searchTextField.getDocument().addDocumentListener(searchFieldListener);
Lines 226-244 Link Here
226
        if (searchPanel != null || !isEnabled()) {
359
        if (searchPanel != null || !isEnabled()) {
227
            return;
360
            return;
228
        }
361
        }
229
        /*
230
        TreeView previousSearchField = lastSearchField.get();
231
        if (previousSearchField != null && previousSearchField != this) {
232
            previousSearchField.removeSearchField();
233
        }
234
        */
235
        //JViewport vp = getViewport();
236
        //originalScrollMode = vp.getScrollMode();
237
        //vp.setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
238
        searchTextField.setOriginalFocusOwner();
362
        searchTextField.setOriginalFocusOwner();
239
        searchTextField.setFont(component.getFont());
363
        searchTextField.setFont(component.getFont());
240
        searchPanel = new SearchPanel();
364
        searchPanel = new SearchPanel();
241
        //JLabel lbl = new JLabel(NbBundle.getMessage(TreeView.class, "LBL_QUICKSEARCH")); //NOI18N
242
        final JLabel lbl;
365
        final JLabel lbl;
243
        if (popupMenu != null) {
366
        if (popupMenu != null) {
244
            lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND_WITH_MENU, false));
367
            lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND_WITH_MENU, false));
Lines 255-260 Link Here
255
        } else {
378
        } else {
256
            lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND, false));
379
            lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND, false));
257
        }
380
        }
381
        if (asynchronous) {
382
            animationTimer = new AnimationTimer(lbl, lbl.getIcon());
383
        } else {
384
            animationTimer = null;
385
        }
258
        searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS));
386
        searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS));
259
        searchPanel.add(lbl);
387
        searchPanel.add(lbl);
260
        searchPanel.add(searchTextField);
388
        searchPanel.add(searchTextField);
Lines 263-274 Link Here
263
        searchTextField.setMaximumSize(searchTextField.getPreferredSize());
391
        searchTextField.setMaximumSize(searchTextField.getPreferredSize());
264
        searchTextField.putClientProperty("JTextField.variant", "search"); //NOI18N
392
        searchTextField.putClientProperty("JTextField.variant", "search"); //NOI18N
265
        lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
393
        lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
266
        //JToggleButton matchCaseButton = new JToggleButton("aA");
267
        //matchCaseButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
268
        //searchPanel.add(matchCaseButton);
269
        if (component instanceof JScrollPane) {
270
        //    ((JScrollPane) component).getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
271
        }
272
        if (constraints == null) {
394
        if (constraints == null) {
273
            component.add(searchPanel);
395
            component.add(searchPanel);
274
        } else {
396
        } else {
Lines 284-298 Link Here
284
        if (searchPanel == null) {
406
        if (searchPanel == null) {
285
            return;
407
            return;
286
        }
408
        }
287
        component.remove(searchPanel);
409
        if (animationTimer != null) {
410
            animationTimer.stopProgressAnimation();
411
        }
412
        Component sp = searchPanel;
288
        searchPanel = null;
413
        searchPanel = null;
289
        //getViewport().setScrollMode(originalScrollMode);
414
        component.remove(sp);
290
        component.invalidate();
415
        component.invalidate();
291
        component.revalidate();
416
        component.revalidate();
292
        component.repaint();
417
        component.repaint();
293
    }
418
    }
294
    
419
    
295
    public static String findMaxCommonSubstring(String str1, String str2, boolean ignoreCase) {
420
    /** Accessed from test. */
421
    JTextField getSearchField() {
422
        return searchTextField;
423
    }
424
    
425
    /**
426
     * Utility method, that finds a greatest common prefix of two supplied
427
     * strings.
428
     * 
429
     * @param str1 The first string
430
     * @param str2 The second string
431
     * @param ignoreCase Whether to ignore case in the comparisons
432
     * @return The greatest common prefix of the two strings.
433
     */
434
    public static String findMaxPrefix(String str1, String str2, boolean ignoreCase) {
296
        int n1 = str1.length();
435
        int n1 = str1.length();
297
        int n2 = str2.length();
436
        int n2 = str2.length();
298
        int i = 0;
437
        int i = 0;
Lines 315-339 Link Here
315
        }
454
        }
316
        return str1.substring(0, i);
455
        return str1.substring(0, i);
317
    }
456
    }
457
    
458
    private final static class AnimationTimer {
459
        
460
        private final JLabel jLabel;
461
        private final Icon findIcon;
462
        private final Timer animationTimer;
463
        
464
        public AnimationTimer(final JLabel jLabel, Icon findIcon) {
465
            this.jLabel = jLabel;
466
            this.findIcon = findIcon;
467
            animationTimer = new Timer(100, new ActionListener() {
318
468
319
    public static interface QuickSearchListener {
469
                ImageIcon icons[];
470
                int index = 0;
471
472
                @Override
473
                public void actionPerformed(ActionEvent e) {
474
                    if (icons == null) {
475
                        icons = new ImageIcon[8];
476
                        for (int i = 0; i < 8; i++) {
477
                            icons[i] = ImageUtilities.loadImageIcon("org/openide/awt/resources/quicksearch/progress_" + i + ".png", false);  //NOI18N
478
                        }
479
                    }
480
                    jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 6));
481
                    jLabel.setIcon(icons[index]);
482
                    //mac os x
483
                    jLabel.repaint();
484
485
                    index = (index + 1) % 8;
486
                }
487
            });
488
        }
320
        
489
        
321
        void quickSearchUpdate(String searchText);
490
        public void startProgressAnimation() {
491
            if (animationTimer != null && !animationTimer.isRunning()) {
492
                animationTimer.start();
493
            }
494
        }
495
496
        public void stopProgressAnimation() {
497
            if (animationTimer != null && animationTimer.isRunning()) {
498
                animationTimer.stop();
499
                jLabel.setIcon(findIcon);
500
                jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
501
            }
502
        }
503
504
    }
505
506
    private class LazyFire implements Runnable {
322
        
507
        
323
        void showNextSelection(Bias bias);
508
        private final QS_FIRE fire;
509
        private final QuickSearchListener[] qsls;
510
        private final String searchText;
511
        private final boolean forward;
512
        private final DataContentHandlerFactory newPrefixSetter;
324
        
513
        
325
        String findMaxPrefix(String prefix);
514
        LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, String searchText) {
515
            this(fire, qsls, searchText, true, null);
516
        }
326
        
517
        
327
        void quickSearchConfirmed();
518
        LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, boolean forward) {
519
            this(fire, qsls, null, forward);
520
        }
328
        
521
        
329
        void quickSearchCanceled();
522
        LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, String searchText, boolean forward) {
523
            this(fire, qsls, searchText, forward, null);
524
        }
525
        
526
        LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, String searchText,
527
                 DataContentHandlerFactory newPrefixSetter) {
528
            this(fire, qsls, searchText, true, newPrefixSetter);
529
        }
530
        
531
        LazyFire(QS_FIRE fire, QuickSearchListener[] qsls, String searchText, boolean forward,
532
                 DataContentHandlerFactory newPrefixSetter) {
533
            this.fire = fire;
534
            this.qsls = qsls;
535
            this.searchText = searchText;
536
            this.forward = forward;
537
            this.newPrefixSetter = newPrefixSetter;
538
            animationTimer.startProgressAnimation();
539
        }
330
540
541
        @Override
542
        public void run() {
543
            try {
544
            switch (fire) {
545
                case UPDATE:    fireQuickSearchUpdate(qsls, searchText);
546
                                break;
547
                case NEXT:      fireShowNextSelection(qsls, forward);
548
                                break;
549
                case MAX:       String mp = findMaxPrefix(qsls, searchText);
550
                                newPrefixSetter.createDataContentHandler(mp);
551
                                break;
552
            }
553
            } finally {
554
                animationTimer.stopProgressAnimation();
555
            }
556
        }
331
    }
557
    }
332
    
558
    
333
    private static class SearchPanel extends JPanel {
559
    private static class SearchPanel extends JPanel {
334
        
560
        
561
        public static final boolean isAquaLaF =
562
                "Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N
563
    
335
        public SearchPanel() {
564
        public SearchPanel() {
336
            if (ViewUtil.isAquaLaF) {
565
            if (isAquaLaF) {
337
                setBorder(BorderFactory.createEmptyBorder(9,6,8,2));
566
                setBorder(BorderFactory.createEmptyBorder(9,6,8,2));
338
            } else {
567
            } else {
339
                setBorder(BorderFactory.createEmptyBorder(2,6,2,2));
568
                setBorder(BorderFactory.createEmptyBorder(2,6,2,2));
Lines 343-351 Link Here
343
572
344
        @Override
573
        @Override
345
        protected void paintComponent(Graphics g) {
574
        protected void paintComponent(Graphics g) {
346
            if (ViewUtil.isAquaLaF && g instanceof Graphics2D) {
575
            if (isAquaLaF && g instanceof Graphics2D) {
347
                Graphics2D g2d = (Graphics2D) g;
576
                Graphics2D g2d = (Graphics2D) g;
348
                g2d.setPaint(new GradientPaint(0, 0, UIManager.getColor("NbExplorerView.quicksearch.background.top"),
577
                g2d.setPaint(new GradientPaint(0, 0, UIManager.getColor("NbExplorerView.quicksearch.background.top"), //NOI18N
349
                        0, getHeight(), UIManager.getColor("NbExplorerView.quicksearch.background.bottom")));//NOI18N
578
                        0, getHeight(), UIManager.getColor("NbExplorerView.quicksearch.background.bottom")));//NOI18N
350
                g2d.fillRect(0, 0, getWidth(), getHeight());
579
                g2d.fillRect(0, 0, getWidth(), getHeight());
351
                g2d.setColor(UIManager.getColor("NbExplorerView.quicksearch.border")); //NOI18N
580
                g2d.setColor(UIManager.getColor("NbExplorerView.quicksearch.border")); //NOI18N
Lines 449-471 Link Here
449
                fireQuickSearchCanceled();
678
                fireQuickSearchCanceled();
450
                e.consume();
679
                e.consume();
451
            } else if (keyCode == KeyEvent.VK_UP || (keyCode == KeyEvent.VK_F3 && e.isShiftDown())) {
680
            } else if (keyCode == KeyEvent.VK_UP || (keyCode == KeyEvent.VK_F3 && e.isShiftDown())) {
452
                fireShowNextSelection(Bias.Backward);
681
                fireShowNextSelection(false);
453
                // Stop processing the event here. Otherwise it's dispatched
682
                // Stop processing the event here. Otherwise it's dispatched
454
                // to the tree too (which scrolls)
683
                // to the tree too (which scrolls)
455
                e.consume();
684
                e.consume();
456
            } else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_F3) {
685
            } else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_F3) {
457
                fireShowNextSelection(Bias.Forward);
686
                fireShowNextSelection(true);
458
                // Stop processing the event here. Otherwise it's dispatched
687
                // Stop processing the event here. Otherwise it's dispatched
459
                // to the tree too (which scrolls)
688
                // to the tree too (which scrolls)
460
                e.consume();
689
                e.consume();
461
            } else if (keyCode == KeyEvent.VK_TAB) {
690
            } else if (keyCode == KeyEvent.VK_TAB) {
462
                String maxPrefix = findMaxPrefix(searchTextField.getText());
691
                findMaxPrefix(searchTextField.getText(), new DataContentHandlerFactory() {
463
                ignoreEvents = true;
692
                    @Override
464
                try {
693
                    public DataContentHandler createDataContentHandler(final String maxPrefix) {
465
                    searchTextField.setText(maxPrefix);
694
                        if (!SwingUtilities.isEventDispatchThread()) {
466
                } finally {
695
                            SwingUtilities.invokeLater(new Runnable() {
467
                    ignoreEvents = false;
696
                                @Override
468
                }
697
                                public void run() {
698
                                    createDataContentHandler(maxPrefix);
699
                                }
700
                            });
701
                            return null;
702
                        }
703
                        ignoreEvents = true;
704
                        try {
705
                            searchTextField.setText(maxPrefix);
706
                        } finally {
707
                            ignoreEvents = false;
708
                        }
709
                        return null;
710
                    }
711
                });
469
712
470
                e.consume();
713
                e.consume();
471
            } else if (keyCode == KeyEvent.VK_ENTER) {
714
            } else if (keyCode == KeyEvent.VK_ENTER) {
Lines 506-513 Link Here
506
            if (oppositeComponent == searchTextField) {
749
            if (oppositeComponent == searchTextField) {
507
                return ;
750
                return ;
508
            }
751
            }
509
            removeSearchField();
752
            if (searchPanel != null) {
510
            fireQuickSearchConfirmed();
753
                removeSearchField();
754
                fireQuickSearchConfirmed();
755
            }
511
        }
756
        }
512
    }
757
    }
513
758
(-)a/openide.awt/src/org/openide/awt/QuickSearchListener.java (+100 lines)
Line 0 Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2012 Oracle and/or its affiliates. All rights reserved.
5
 *
6
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
7
 * Other names may be trademarks of their respective owners.
8
 *
9
 * The contents of this file are subject to the terms of either the GNU
10
 * General Public License Version 2 only ("GPL") or the Common
11
 * Development and Distribution License("CDDL") (collectively, the
12
 * "License"). You may not use this file except in compliance with the
13
 * License. You can obtain a copy of the License at
14
 * http://www.netbeans.org/cddl-gplv2.html
15
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
16
 * specific language governing permissions and limitations under the
17
 * License.  When distributing the software, include this License Header
18
 * Notice in each file and include the License file at
19
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
20
 * particular file as subject to the "Classpath" exception as provided
21
 * by Oracle in the GPL Version 2 section of the License file that
22
 * accompanied this code. If applicable, add the following below the
23
 * License Header, with the fields enclosed by brackets [] replaced by
24
 * your own identifying information:
25
 * "Portions Copyrighted [year] [name of copyright owner]"
26
 *
27
 * If you wish your version of this file to be governed by only the CDDL
28
 * or only the GPL Version 2, indicate your decision by adding
29
 * "[Contributor] elects to include this software in this distribution
30
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
31
 * single choice of license, a recipient has the option to distribute
32
 * your version of this file under either the CDDL, the GPL Version 2 or
33
 * to extend the choice of license to its licensees as provided above.
34
 * However, if you add GPL Version 2 code and therefore, elected the GPL
35
 * Version 2 license, then the option applies only if the new code is
36
 * made subject to such option by the copyright holder.
37
 *
38
 * Contributor(s):
39
 *
40
 * Portions Copyrighted 2012 Sun Microsystems, Inc.
41
 */
42
package org.openide.awt;
43
44
/**
45
 * Listener that is notified with the submissions to the quick search field.
46
 * 
47
 * @author Martin Entlicher
48
 * @since 7.43
49
 */
50
public interface QuickSearchListener {
51
    
52
    /**
53
     * Called with an updated search text.
54
     * When {@link QuickSearch#isAsynchronous()} is <code>false</code>
55
     * it's called in EQ thread, otherwise, it's called in a background thread.
56
     * The client should update the visual representation of the search results
57
     * and then return.<p>
58
     * This method is called to initiate and update the search process.
59
     * @param searchText The new text to search for.
60
     */
61
    void quickSearchUpdate(String searchText);
62
63
    /**
64
     * Called to select a next occurrence of the search result.
65
     * When {@link QuickSearch#isAsynchronous()} is <code>false</code>
66
     * it's called in EQ thread, otherwise, it's called in a background thread.
67
     * The client should update the visual representation of the search results
68
     * and then return.<p>
69
     * @param forward The direction of the next search result.
70
     *                <code>true</code> for forward direction,
71
     *                <code>false</code> for backward direction.
72
     */
73
    void showNextSelection(boolean forward);
74
75
    /**
76
     * Find the maximum prefix among the search results, that starts with the provided string.
77
     * This method is called when user press TAB in the search field, to auto-complete
78
     * the maximum prefix.
79
     * When {@link QuickSearch#isAsynchronous()} is <code>false</code>
80
     * it's called in EQ thread, otherwise, it's called in a background thread.
81
     * Utility method {@link QuickSearch#findMaxCommonSubstring(java.lang.String, java.lang.String, boolean)}
82
     * can be used by the implementation.
83
     * @param prefix The prefix to start with
84
     * @return The maximum prefix.
85
     */
86
    String findMaxPrefix(String prefix);
87
88
    /**
89
     * Called when the quick search is confirmed by the user.
90
     * This method is called in EQ thread always.
91
     */
92
    void quickSearchConfirmed();
93
94
    /**
95
     * Called when the quick search is canceled by the user.
96
     * This method is called in EQ thread always.
97
     */
98
    void quickSearchCanceled();
99
100
}
(-)a/openide.awt/test/unit/src/org/openide/awt/QuickSearchTest.java (+658 lines)
Line 0 Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2012 Oracle and/or its affiliates. All rights reserved.
5
 *
6
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
7
 * Other names may be trademarks of their respective owners.
8
 *
9
 * The contents of this file are subject to the terms of either the GNU
10
 * General Public License Version 2 only ("GPL") or the Common
11
 * Development and Distribution License("CDDL") (collectively, the
12
 * "License"). You may not use this file except in compliance with the
13
 * License. You can obtain a copy of the License at
14
 * http://www.netbeans.org/cddl-gplv2.html
15
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
16
 * specific language governing permissions and limitations under the
17
 * License.  When distributing the software, include this License Header
18
 * Notice in each file and include the License file at
19
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
20
 * particular file as subject to the "Classpath" exception as provided
21
 * by Oracle in the GPL Version 2 section of the License file that
22
 * accompanied this code. If applicable, add the following below the
23
 * License Header, with the fields enclosed by brackets [] replaced by
24
 * your own identifying information:
25
 * "Portions Copyrighted [year] [name of copyright owner]"
26
 *
27
 * If you wish your version of this file to be governed by only the CDDL
28
 * or only the GPL Version 2, indicate your decision by adding
29
 * "[Contributor] elects to include this software in this distribution
30
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
31
 * single choice of license, a recipient has the option to distribute
32
 * your version of this file under either the CDDL, the GPL Version 2 or
33
 * to extend the choice of license to its licensees as provided above.
34
 * However, if you add GPL Version 2 code and therefore, elected the GPL
35
 * Version 2 license, then the option applies only if the new code is
36
 * made subject to such option by the copyright holder.
37
 *
38
 * Contributor(s):
39
 *
40
 * Portions Copyrighted 2012 Sun Microsystems, Inc.
41
 */
42
package org.openide.awt;
43
44
import java.awt.Component;
45
import java.awt.KeyboardFocusManager;
46
import java.awt.event.KeyEvent;
47
import java.awt.event.KeyListener;
48
import java.lang.reflect.InvocationTargetException;
49
import java.lang.reflect.Method;
50
import java.util.ArrayList;
51
import java.util.List;
52
import javax.swing.JComponent;
53
import javax.swing.JFrame;
54
import javax.swing.SwingUtilities;
55
import org.junit.After;
56
import org.junit.AfterClass;
57
import org.junit.Before;
58
import org.junit.Test;
59
import static org.junit.Assert.*;
60
import org.junit.BeforeClass;
61
62
/**
63
 * Test of QuickSearch.
64
 * 
65
 * @author Martin Entlicher
66
 */
67
public class QuickSearchTest {
68
    
69
    public QuickSearchTest() {
70
    }
71
72
    @BeforeClass
73
    public static void setUpClass() throws Exception {
74
    }
75
76
    @AfterClass
77
    public static void tearDownClass() throws Exception {
78
    }
79
    
80
    @Before
81
    public void setUp() {
82
    }
83
    
84
    @After
85
    public void tearDown() {
86
    }
87
    
88
    /**
89
     * Test of attach and detach methods, of class QuickSearch.
90
     */
91
    @Test
92
    public void testAttachDetach() {
93
        TestComponent component = new TestComponent();
94
        Object constraints = null;
95
        QuickSearch qs = QuickSearch.attach(component, constraints);
96
        assertEquals("One added key listener is expected after attach", 1, component.addedKeyListeners.size());
97
        assertTrue(qs.isEnabled());
98
        assertFalse(qs.isAsynchronous());
99
        qs.detach();
100
        assertEquals("No key listener is expected after detach", 0, component.addedKeyListeners.size());
101
    }
102
103
    /**
104
     * Test of isEnabled and setEnabled methods, of class QuickSearch.
105
     */
106
    @Test
107
    public void testIsEnabled() {
108
        TestComponent component = new TestComponent();
109
        Object constraints = null;
110
        QuickSearch qs = QuickSearch.attach(component, constraints);
111
        assertTrue(qs.isEnabled());
112
        qs.setEnabled(false);
113
        assertEquals("No key listener is expected after setEnabled(false)", 0, component.addedKeyListeners.size());
114
        assertFalse(qs.isEnabled());
115
        qs.setEnabled(true);
116
        assertTrue(qs.isEnabled());
117
        assertEquals("One added key listener is expected after setEnabled(true)", 1, component.addedKeyListeners.size());
118
        qs.detach();
119
    }
120
121
    /**
122
     * Test of the addition of quick search component.
123
     */
124
    @Test
125
    public void testQuickSearchAdd() {
126
        if (!SwingUtilities.isEventDispatchThread()) {
127
            try {
128
                SwingUtilities.invokeAndWait(new Runnable() {
129
                    @Override
130
                    public void run() {
131
                        testQuickSearchAdd();
132
                    }
133
                });
134
            } catch (InterruptedException iex) {
135
                fail("interrupted.");
136
            } catch (InvocationTargetException itex) {
137
                Throwable cause = itex.getCause();
138
                if (cause instanceof AssertionError) {
139
                    throw (AssertionError) cause;
140
                }
141
                itex.getCause().printStackTrace();
142
                throw new AssertionError(cause);
143
            }
144
            return;
145
        }
146
        TestComponent component = new TestComponent();
147
        Object constraints = null;
148
        QuickSearch qs = QuickSearch.attach(component, constraints);
149
        component.addNotify();
150
        KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
151
        //KeyboardFocusManager.getCurrentKeyboardFocusManager().setGlobalFocusOwner(component);
152
        try {
153
            Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class);
154
            setGlobalFocusOwner.setAccessible(true);
155
            setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), component);
156
        } catch (Exception ex) {
157
            ex.printStackTrace();
158
            throw new AssertionError(ex);
159
        }
160
        component.dispatchEvent(ke);
161
        assertNotNull(component.added);
162
        assertNull(component.constraints);
163
        qs.detach();
164
        assertNull(component.added);
165
        
166
        constraints = new Object();
167
        qs = QuickSearch.attach(component, constraints);
168
        ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
169
        component.dispatchEvent(ke);
170
        assertNotNull(component.added);
171
        assertEquals(constraints, component.constraints);
172
        qs.detach();
173
        assertNull(component.added);
174
    }
175
    
176
    /**
177
     * Test of the quick search listener.
178
     */
179
    @Test
180
    public void testQuickSearchListener() {
181
        if (!SwingUtilities.isEventDispatchThread()) {
182
            try {
183
                SwingUtilities.invokeAndWait(new Runnable() {
184
                    @Override
185
                    public void run() {
186
                        testQuickSearchListener();
187
                    }
188
                });
189
            } catch (InterruptedException iex) {
190
                fail("interrupted.");
191
            } catch (InvocationTargetException itex) {
192
                Throwable cause = itex.getCause();
193
                if (cause instanceof AssertionError) {
194
                    throw (AssertionError) cause;
195
                }
196
                itex.getCause().printStackTrace();
197
                throw new AssertionError(cause);
198
            }
199
            return;
200
        }
201
        TestComponent component = new TestComponent();
202
        Object constraints = null;
203
        QuickSearch qs = QuickSearch.attach(component, constraints);
204
        component.addNotify();
205
        final String[] searchTextPtr = new String[] { null };
206
        final Boolean[] biasPtr = new Boolean[] { null };
207
        final boolean[] confirmedPtr = new boolean[] { false };
208
        final boolean[] canceledPtr = new boolean[] { false };
209
        QuickSearchListener qsl = new QuickSearchListener() {
210
211
            @Override
212
            public void quickSearchUpdate(String searchText) {
213
                assertTrue(SwingUtilities.isEventDispatchThread());
214
                searchTextPtr[0] = searchText;
215
            }
216
217
            @Override
218
            public void showNextSelection(boolean forward) {
219
                assertTrue(SwingUtilities.isEventDispatchThread());
220
                biasPtr[0] = forward;
221
            }
222
223
            @Override
224
            public String findMaxPrefix(String prefix) {
225
                assertTrue(SwingUtilities.isEventDispatchThread());
226
                return prefix + "endPrefix";
227
            }
228
229
            @Override
230
            public void quickSearchConfirmed() {
231
                assertTrue(SwingUtilities.isEventDispatchThread());
232
                confirmedPtr[0] = true;
233
            }
234
235
            @Override
236
            public void quickSearchCanceled() {
237
                assertTrue(SwingUtilities.isEventDispatchThread());
238
                canceledPtr[0] = true;
239
            }
240
        };
241
        qs.addQuickSearchListener(qsl);
242
        // Test that a key event passed to the component triggers the quick search:
243
        try {
244
            Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class);
245
            setGlobalFocusOwner.setAccessible(true);
246
            setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), component);
247
        } catch (Exception ex) {
248
            ex.printStackTrace();
249
            throw new AssertionError(ex);
250
        }
251
        KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
252
        component.dispatchEvent(ke);
253
        assertEquals("A", qs.getSearchField().getText());
254
        assertEquals("A", searchTextPtr[0]);
255
        assertNull(biasPtr[0]);
256
        
257
        // Test that further key events passed to the quick search field trigger the quick search listener:
258
        qs.getSearchField().setCaretPosition(1);
259
        try {
260
            Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class);
261
            setGlobalFocusOwner.setAccessible(true);
262
            setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), qs.getSearchField());
263
        } catch (Exception ex) {
264
            ex.printStackTrace();
265
            throw new AssertionError(ex);
266
        }
267
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'b');
268
        qs.getSearchField().dispatchEvent(ke);
269
        assertEquals("Ab", searchTextPtr[0]);
270
        
271
        // Test the up/down keys resulting to selection navigation:
272
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_UP, (char) KeyEvent.VK_UP);
273
        qs.getSearchField().dispatchEvent(ke);
274
        assertEquals(Boolean.FALSE, biasPtr[0]);
275
        
276
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_DOWN, (char) KeyEvent.VK_DOWN);
277
        qs.getSearchField().dispatchEvent(ke);
278
        assertEquals(Boolean.TRUE, biasPtr[0]);
279
        
280
        // Test that tab adds max prefix:
281
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_TAB, '\t');
282
        qs.getSearchField().dispatchEvent(ke);
283
        assertEquals("AbendPrefix", qs.getSearchField().getText());
284
        
285
        // Test that we get no events when quick search listener is detached:
286
        qs.removeQuickSearchListener(qsl);
287
        qs.getSearchField().setCaretPosition(2);
288
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'c');
289
        qs.getSearchField().dispatchEvent(ke);
290
        assertEquals("AbcendPrefix", qs.getSearchField().getText());
291
        assertEquals("Ab", searchTextPtr[0]);
292
        
293
        // Test the quick search confirmation on Enter key:
294
        qs.addQuickSearchListener(qsl);
295
        assertFalse(confirmedPtr[0]);
296
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ENTER, '\n');
297
        qs.getSearchField().dispatchEvent(ke);
298
        assertTrue(confirmedPtr[0]);
299
        
300
        // Test the quick search cancel on ESC key:
301
        ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
302
        component.dispatchEvent(ke);
303
        assertEquals("A", searchTextPtr[0]);
304
        assertFalse(canceledPtr[0]);
305
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ESCAPE, (char) 27);
306
        qs.getSearchField().dispatchEvent(ke);
307
        assertTrue(canceledPtr[0]);
308
    }
309
    
310
    enum sync { W, N } // Wait, Notify
311
312
    /**
313
     * Test of asynchronous calls, of class QuickSearch.
314
     */
315
    @Test
316
    public void testAsynchronous() {
317
        final TestComponent[] componentPtr = new TestComponent[] { null };
318
        final QuickSearch[] qsPtr = new QuickSearch[] { null };
319
        try {
320
            SwingUtilities.invokeAndWait(new Runnable() {
321
                @Override
322
                public void run() {
323
                    componentPtr[0] = new TestComponent();
324
                    qsPtr[0] = QuickSearch.attach(componentPtr[0], null);
325
                    componentPtr[0].addNotify();
326
                }
327
            });
328
        } catch (InterruptedException iex) {
329
            fail("interrupted.");
330
        } catch (InvocationTargetException itex) {
331
            Throwable cause = itex.getCause();
332
            if (cause instanceof AssertionError) {
333
                throw (AssertionError) cause;
334
            }
335
            itex.getCause().printStackTrace();
336
            throw new AssertionError(cause);
337
        }
338
        final String[] searchTextPtr = new String[] { null };
339
        final Boolean[] biasPtr = new Boolean[] { null };
340
        final Object findMaxPrefixLock = new Object();
341
        final boolean[] confirmedPtr = new boolean[] { false };
342
        final boolean[] canceledPtr = new boolean[] { false };
343
        final boolean[] asynchronousPtr = new boolean[] { false };
344
        final sync[] syncPtr = new sync[] { null };
345
        QuickSearchListener qsl = new QuickSearchListener() {
346
347
            @Override
348
            public void quickSearchUpdate(String searchText) {
349
                assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread());
350
                synchronized(searchTextPtr) {
351
                    if (syncPtr[0] == null) {
352
                        syncPtr[0] = sync.W;
353
                        // Wait for the notification first
354
                        try { searchTextPtr.wait(); } catch (InterruptedException iex) {}
355
                    }
356
                    searchTextPtr[0] = searchText;
357
                    searchTextPtr.notifyAll();
358
                    syncPtr[0] = null;
359
                }
360
            }
361
362
            @Override
363
            public void showNextSelection(boolean forward) {
364
                assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread());
365
                synchronized(biasPtr) {
366
                    if (syncPtr[0] == null) {
367
                        syncPtr[0] = sync.W;
368
                        // Wait for the notification first
369
                        try { biasPtr.wait(); } catch (InterruptedException iex) {}
370
                    }
371
                    biasPtr[0] = forward;
372
                    biasPtr.notifyAll();
373
                    syncPtr[0] = null;
374
                }
375
            }
376
377
            @Override
378
            public String findMaxPrefix(String prefix) {
379
                assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread());
380
                synchronized(findMaxPrefixLock) {
381
                    if (syncPtr[0] == null) {
382
                        syncPtr[0] = sync.W;
383
                        // Wait for the notification first
384
                        try { findMaxPrefixLock.wait(); } catch (InterruptedException iex) {}
385
                    }
386
                    prefix = prefix + "endPrefix";
387
                    findMaxPrefixLock.notifyAll();
388
                    syncPtr[0] = null;
389
                }
390
                return prefix;
391
            }
392
393
            @Override
394
            public void quickSearchConfirmed() {
395
                assertTrue(SwingUtilities.isEventDispatchThread());
396
                confirmedPtr[0] = true;
397
            }
398
399
            @Override
400
            public void quickSearchCanceled() {
401
                assertTrue(SwingUtilities.isEventDispatchThread());
402
                canceledPtr[0] = true;
403
            }
404
        };
405
        qsPtr[0].addQuickSearchListener(qsl);
406
        assertFalse(qsPtr[0].isAsynchronous());
407
        qsPtr[0].setAsynchronous(true);
408
        asynchronousPtr[0] = true;
409
        assertTrue(qsPtr[0].isAsynchronous());
410
        
411
        // Test that a key event passed to the component triggers the asynchronous quick search:
412
        try {
413
            SwingUtilities.invokeAndWait(new Runnable() {
414
                @Override
415
                public void run() {
416
                    try {
417
                        Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class);
418
                        setGlobalFocusOwner.setAccessible(true);
419
                        setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), componentPtr[0]);
420
                    } catch (Exception ex) {
421
                        ex.printStackTrace();
422
                        throw new AssertionError(ex);
423
                    }
424
                    KeyEvent ke = new KeyEvent(componentPtr[0], KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
425
                    componentPtr[0].dispatchEvent(ke);
426
                }
427
            });
428
        } catch (InterruptedException iex) {
429
            fail("interrupted.");
430
        } catch (InvocationTargetException itex) {
431
            Throwable cause = itex.getCause();
432
            if (cause instanceof AssertionError) {
433
                throw (AssertionError) cause;
434
            }
435
            itex.getCause().printStackTrace();
436
            throw new AssertionError(cause);
437
        }
438
        synchronized(searchTextPtr) {
439
            assertNull(searchTextPtr[0]);
440
            syncPtr[0] = sync.N;
441
            searchTextPtr.notifyAll();
442
            // Wait to set the value
443
            try { searchTextPtr.wait(); } catch (InterruptedException iex) {}
444
            assertEquals("A", searchTextPtr[0]);
445
        }
446
        
447
        // Test the up/down keys resulting to asynchronous selection navigation:
448
        try {
449
            SwingUtilities.invokeAndWait(new Runnable() {
450
                @Override
451
                public void run() {
452
                    KeyEvent ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_UP, (char) KeyEvent.VK_UP);
453
                    qsPtr[0].getSearchField().dispatchEvent(ke);
454
455
                    ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_DOWN, (char) KeyEvent.VK_DOWN);
456
                    qsPtr[0].getSearchField().dispatchEvent(ke);
457
                }
458
            });
459
        } catch (InterruptedException iex) {
460
            fail("interrupted.");
461
        } catch (InvocationTargetException itex) {
462
            Throwable cause = itex.getCause();
463
            if (cause instanceof AssertionError) {
464
                throw (AssertionError) cause;
465
            }
466
            itex.getCause().printStackTrace();
467
            throw new AssertionError(cause);
468
        }
469
        synchronized(biasPtr) {
470
            assertNull(biasPtr[0]);
471
            syncPtr[0] = sync.N;
472
            biasPtr.notifyAll();
473
            // Wait to set the value
474
            try { biasPtr.wait(); } catch (InterruptedException iex) {}
475
            assertEquals(Boolean.FALSE, biasPtr[0]);
476
        }
477
        synchronized(biasPtr) {
478
            assertEquals(Boolean.FALSE, biasPtr[0]);
479
            syncPtr[0] = sync.N;
480
            biasPtr.notifyAll();
481
            // Wait to set the value
482
            try { biasPtr.wait(); } catch (InterruptedException iex) {}
483
            assertEquals(Boolean.TRUE, biasPtr[0]);
484
        }
485
        
486
        // Test that tab adds max prefix asynchronously:
487
        try {
488
            SwingUtilities.invokeAndWait(new Runnable() {
489
                @Override
490
                public void run() {
491
                    KeyEvent ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_TAB, '\t');
492
                    qsPtr[0].getSearchField().dispatchEvent(ke);
493
                }
494
            });
495
        } catch (InterruptedException iex) {
496
            fail("interrupted.");
497
        } catch (InvocationTargetException itex) {
498
            Throwable cause = itex.getCause();
499
            if (cause instanceof AssertionError) {
500
                throw (AssertionError) cause;
501
            }
502
            itex.getCause().printStackTrace();
503
            throw new AssertionError(cause);
504
        }
505
        synchronized(findMaxPrefixLock) {
506
            assertEquals("A", qsPtr[0].getSearchField().getText());
507
            syncPtr[0] = sync.N;
508
            findMaxPrefixLock.notifyAll();
509
            // Wait to set the value
510
            try { findMaxPrefixLock.wait(); } catch (InterruptedException iex) {}
511
            // Can not test it immediatelly, the text is updated in AWT
512
            // assertEquals("AendPrefix", qsPtr[0].getSearchField().getText());
513
        }
514
        try { Thread.sleep(200); } catch (InterruptedException iex) {}
515
        try {
516
            SwingUtilities.invokeAndWait(new Runnable() {
517
                @Override
518
                public void run() {
519
                    assertEquals("AendPrefix", qsPtr[0].getSearchField().getText());
520
                }
521
            });
522
        } catch (InterruptedException iex) {
523
            fail("interrupted.");
524
        } catch (InvocationTargetException itex) {
525
            Throwable cause = itex.getCause();
526
            if (cause instanceof AssertionError) {
527
                throw (AssertionError) cause;
528
            }
529
            itex.getCause().printStackTrace();
530
            throw new AssertionError(cause);
531
        }
532
    }
533
    
534
    /**
535
     * Test of processKeyEvent method, of class QuickSearch.
536
     */
537
    @Test
538
    public void testProcessKeyEvent() {
539
        TestComponent component = new TestComponent();
540
        Object constraints = null;
541
        QuickSearch qs = QuickSearch.attach(component, constraints);
542
        final String[] searchTextPtr = new String[] { null };
543
        final Boolean[] biasPtr = new Boolean[] { null };
544
        final boolean[] confirmedPtr = new boolean[] { false };
545
        final boolean[] canceledPtr = new boolean[] { false };
546
        QuickSearchListener qsl = new QuickSearchListener() {
547
548
            @Override
549
            public void quickSearchUpdate(String searchText) {
550
                searchTextPtr[0] = searchText;
551
            }
552
553
            @Override
554
            public void showNextSelection(boolean forward) {
555
                biasPtr[0] = forward;
556
            }
557
558
            @Override
559
            public String findMaxPrefix(String prefix) {
560
                return prefix + "endPrefix";
561
            }
562
563
            @Override
564
            public void quickSearchConfirmed() {
565
                confirmedPtr[0] = true;
566
            }
567
568
            @Override
569
            public void quickSearchCanceled() {
570
                canceledPtr[0] = true;
571
            }
572
        };
573
        qs.addQuickSearchListener(qsl);
574
        KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
575
        qs.processKeyEvent(ke);
576
        assertEquals("A", qs.getSearchField().getText());
577
        assertEquals("A", searchTextPtr[0]);
578
        assertNull(biasPtr[0]);
579
        
580
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'b');
581
        qs.processKeyEvent(ke);
582
        assertEquals("Ab", qs.getSearchField().getText());
583
        assertEquals("Ab", searchTextPtr[0]);
584
    }
585
586
    /**
587
     * Test of findMaxCommonSubstring method, of class QuickSearch.
588
     */
589
    @Test
590
    public void testFindMaxCommonSubstring() {
591
        System.out.println("findMaxCommonSubstring");
592
        String str1 = "annotation";
593
        String str2 = "antenna";
594
        boolean ignoreCase = false;
595
        String expResult = "an";
596
        String result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase);
597
        assertEquals(expResult, result);
598
        str1 = "Annotation";
599
        expResult = "";
600
        result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase);
601
        assertEquals(expResult, result);
602
        str1 = "AbCdEf";
603
        str2 = "AbCxxx";
604
        expResult = "AbC";
605
        result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase);
606
        assertEquals(expResult, result);
607
    }
608
    
609
    private static final class TestComponent extends JComponent {
610
        
611
        List<KeyListener> addedKeyListeners = new ArrayList<KeyListener>();
612
        Component added;
613
        Object constraints;
614
        
615
        public TestComponent() {
616
            new JFrame().add(this); // To have a parent
617
        }
618
619
        @Override
620
        public boolean isShowing() {
621
            return true;
622
        }
623
        
624
        @Override
625
        public Component add(Component comp) {
626
            this.added = comp;
627
            return super.add(comp);
628
        }
629
630
        @Override
631
        public void add(Component comp, Object constraints) {
632
            this.added = comp;
633
            this.constraints = constraints;
634
            super.add(comp, constraints);
635
        }
636
637
        @Override
638
        public void remove(Component comp) {
639
            if (comp == this.added) {
640
                this.added = null;
641
            }
642
            super.remove(comp);
643
        }
644
        
645
        @Override
646
        public synchronized void addKeyListener(KeyListener l) {
647
            addedKeyListeners.add(l);
648
            super.addKeyListener(l);
649
        }
650
651
        @Override
652
        public synchronized void removeKeyListener(KeyListener l) {
653
            addedKeyListeners.remove(l);
654
            super.removeKeyListener(l);
655
        }
656
        
657
    }
658
}

Return to bug 208794