Please use the Apache issue tracking system for new NetBeans issues (https://issues.apache.org/jira/projects/NETBEANS0/issues) !!
View | Details | Raw Unified | Return to bug 208794 | Differences between
and this patch

Collapse All | Expand All

(-)openide.awt/apichanges.xml (+13 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 is added. It 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
      <issue number="208794"/>
65
    </change>
53
    <change id="Actions.forID">
66
    <change id="Actions.forID">
54
        <api name="awt"/>
67
        <api name="awt"/>
55
        <summary>Added <code>Actions.forID</code></summary>
68
        <summary>Added <code>Actions.forID</code></summary>
(-)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
(-)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/>
(-)openide.awt/src/org/openide/awt/QuickSearch.java (+791 lines)
Added 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.*;
45
import java.awt.event.*;
46
import java.lang.ref.WeakReference;
47
import java.util.LinkedList;
48
import java.util.List;
49
import javax.activation.DataContentHandler;
50
import javax.activation.DataContentHandlerFactory;
51
import javax.swing.*;
52
import javax.swing.event.DocumentEvent;
53
import javax.swing.event.DocumentListener;
54
import org.netbeans.api.annotations.common.StaticResource;
55
import org.openide.util.ImageUtilities;
56
import org.openide.util.RequestProcessor;
57
58
/**
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.
62
 * 
63
 * @author Martin Entlicher
64
 * @since 7.43
65
 */
66
public class QuickSearch {
67
    
68
    @StaticResource
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
72
    private static final Object CLIENT_PROPERTY_KEY = new Object();
73
    
74
    private final JComponent component;
75
    private final Object constraints;
76
    private final Callback callback;
77
    private final JMenu popupMenu;
78
    private final boolean asynchronous;
79
    private boolean enabled = true;
80
    private SearchTextField searchTextField;
81
    private KeyAdapter quickSearchKeyAdapter;
82
    private SearchFieldListener searchFieldListener;
83
    private JPanel searchPanel;
84
    private final RequestProcessor rp;
85
    private static enum QS_FIRE { UPDATE, NEXT, MAX }
86
    private AnimationTimer animationTimer;
87
    
88
    private QuickSearch(JComponent component, Object constraints,
89
                        Callback callback, boolean asynchronous, JMenu popupMenu) {
90
        this.component = component;
91
        this.constraints = constraints;
92
        this.callback = callback;
93
        this.asynchronous = asynchronous;
94
        this.popupMenu = popupMenu;
95
        if (asynchronous) {
96
            rp = new RequestProcessor(QuickSearch.class);
97
        } else {
98
            rp = null;
99
        }
100
        setUpSearch();
101
    }
102
    
103
    /**
104
     * Attach quick search to a component with given constraints.
105
     * It listens on key events going to the component and displays a quick search
106
     * field.
107
     * 
108
     * @param component The component to attach to
109
     * @param constraints The constraints that are used to add the search field
110
     * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
111
     * when adding the quick search UI to the component.
112
     * @param callback The call back implementation, which is notified from the
113
     * quick search field submissions.
114
     * @return An instance of QuickSearch class.
115
     */
116
    public static QuickSearch attach(JComponent component, Object constraints,
117
                                     Callback callback) {
118
        return attach(component, constraints, callback, false, null);
119
    }
120
    
121
    /**
122
     * Attach quick search to a component with given constraints.
123
     * It listens on key events going to the component and displays a quick search
124
     * field.
125
     * 
126
     * @param component The component to attach to
127
     * @param constraints The constraints that are used to add the search field
128
     * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
129
     * when adding the quick search UI to the component.
130
     * @param callback The call back implementation, which is notified from the
131
     * quick search field submissions.
132
     * @return An instance of QuickSearch class.
133
     */
134
    public static QuickSearch attach(JComponent component, Object constraints,
135
                                     Callback callback, boolean asynchronous) {
136
        return attach(component, constraints, callback, asynchronous, null);
137
    }
138
    
139
    /**
140
     * Attach quick search to a component with given constraints.
141
     * It listens on key events going to the component and displays a quick search
142
     * field.
143
     * 
144
     * @param component The component to attach to
145
     * @param constraints The constraints that are used to add the search field
146
     * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
147
     * when adding the quick search UI to the component.
148
     * @param callback The call back implementation, which is notified from the
149
     * quick search field submissions.
150
     * @param popupMenu A pop-up menu, that is displayed on the find icon, next to the search
151
     * field. This allows customization of the search criteria. The pop-up menu
152
     * is taken from {@link JMenu#getPopupMenu()}.
153
     * @return An instance of QuickSearch class.
154
     */
155
    public static QuickSearch attach(JComponent component, Object constraints,
156
                                     Callback callback, JMenu popupMenu) {
157
        return attach(component, constraints, callback, false, popupMenu);
158
    }
159
    /**
160
     * Attach quick search to a component with given constraints.
161
     * It listens on key events going to the component and displays a quick search
162
     * field.
163
     * 
164
     * @param component The component to attach to
165
     * @param constraints The constraints that are used to add the search field
166
     * to the component. It's passed to {@link JComponent#add(java.awt.Component, java.lang.Object)}
167
     * when adding the quick search UI to the component.
168
     * @param callback The call back implementation, which is notified from the
169
     * quick search field submissions.
170
     * @param asynchronous Set whether the quick search notifies the call back
171
     * asynchronously, or not.
172
     * By default, Callback is notified synchronously on EQ thread.
173
     * If <code>true</code>, three notification methods are called asynchronously
174
     * on a background thread. These are
175
     * {@link Callback#quickSearchUpdate(java.lang.String)},
176
     * {@link Callback#showNextSelection(javax.swing.text.Position.Bias)},
177
     * {@link Callback#findMaxPrefix(java.lang.String)}.
178
     * If <code>false</code> all methods are called synchronously on EQ thread.
179
     * @param popupMenu A pop-up menu, that is displayed on the find icon, next to the search
180
     * field. This allows customization of the search criteria. The pop-up menu
181
     * is taken from {@link JMenu#getPopupMenu()}.
182
     * @return An instance of QuickSearch class.
183
     */
184
    public static QuickSearch attach(JComponent component, Object constraints,
185
                                     Callback callback, boolean asynchronous, JMenu popupMenu) {
186
        Object qso = component.getClientProperty(CLIENT_PROPERTY_KEY);
187
        if (qso instanceof QuickSearch) {
188
            throw new IllegalStateException("A quick search is attached to this component already, detach it first."); // NOI18N
189
        } else {
190
            QuickSearch qs = new QuickSearch(component, constraints, callback, asynchronous, popupMenu);
191
            component.putClientProperty(CLIENT_PROPERTY_KEY, qs);
192
            return qs;
193
        }
194
    }
195
    
196
    /**
197
     * Detach the quick search from the component it was attached to.
198
     */
199
    public void detach() {
200
        setEnabled(false);
201
        component.putClientProperty(CLIENT_PROPERTY_KEY, null);
202
    }
203
    
204
    /**
205
     * Test whether the quick search is enabled. This is <code>true</code>
206
     * by default.
207
     * @return <code>true</code> when the quick search is enabled,
208
     *         <code>false</code> otherwise.
209
     */
210
    public boolean isEnabled() {
211
        return enabled;
212
    }
213
    
214
    /**
215
     * Set the enabled state of the quick search.
216
     * This allows to activate/deactivate the quick search functionality.
217
     * @param enabled <code>true</code> to enable the quick search,
218
     *                <code>false</code> otherwise.
219
     */
220
    public void setEnabled(boolean enabled) {
221
        if (this.enabled == enabled) {
222
            return ;
223
        }
224
        this.enabled = enabled;
225
        if (enabled) {
226
            component.addKeyListener(quickSearchKeyAdapter);
227
        } else {
228
            removeSearchField();
229
            component.removeKeyListener(quickSearchKeyAdapter);
230
        }
231
    }
232
    
233
    /**
234
     * Process this key event in addition to the key events obtained from the
235
     * component we're attached to.
236
     * @param ke a key event to process.
237
     */
238
    public void processKeyEvent(KeyEvent ke) {
239
        if (searchPanel != null) {
240
            searchTextField.setCaretPosition(searchTextField.getText().length());
241
            searchTextField.processKeyEvent(ke);
242
        } else {
243
            switch(ke.getID()) {
244
                case KeyEvent.KEY_PRESSED:
245
                    quickSearchKeyAdapter.keyPressed(ke);
246
                    break;
247
                case KeyEvent.KEY_RELEASED:
248
                    quickSearchKeyAdapter.keyReleased(ke);
249
                    break;
250
                case KeyEvent.KEY_TYPED:
251
                    quickSearchKeyAdapter.keyTyped(ke);
252
                    break;
253
            }
254
        }
255
    }
256
    
257
    private void fireQuickSearchUpdate(String searchText) {
258
        if (asynchronous) {
259
            rp.post(new LazyFire(QS_FIRE.UPDATE, searchText));
260
        } else {
261
            callback.quickSearchUpdate(searchText);
262
        }
263
    }
264
    
265
    private void fireShowNextSelection(boolean forward) {
266
        if (asynchronous) {
267
            rp.post(new LazyFire(QS_FIRE.NEXT, forward));
268
        } else {
269
            callback.showNextSelection(forward);
270
        }
271
    }
272
    
273
    private void findMaxPrefix(String prefix, DataContentHandlerFactory newPrefixSetter) {
274
        if (asynchronous) {
275
            rp.post(new LazyFire(QS_FIRE.MAX, prefix, newPrefixSetter));
276
        } else {
277
            prefix = callback.findMaxPrefix(prefix);
278
            newPrefixSetter.createDataContentHandler(prefix);
279
        }
280
    }
281
    
282
    private void setUpSearch() {
283
        searchTextField = new SearchTextField();
284
        // create new key listeners
285
        quickSearchKeyAdapter = (
286
            new KeyAdapter() {
287
            @Override
288
                public void keyTyped(KeyEvent e) {
289
                    int modifiers = e.getModifiers();
290
                    int keyCode = e.getKeyCode();
291
                    char c = e.getKeyChar();
292
293
                    //#43617 - don't eat + and -
294
                    //#98634 - and all its duplicates dont't react to space
295
                    if ((c == '+') || (c == '-') || (c==' ')) return; // NOI18N
296
297
                    if (((modifiers > 0) && (modifiers != KeyEvent.SHIFT_MASK)) || e.isActionKey()) {
298
                        return;
299
                    }
300
301
                    if (Character.isISOControl(c) ||
302
                            (keyCode == KeyEvent.VK_SHIFT) ||
303
                            (keyCode == KeyEvent.VK_ESCAPE)) return;
304
305
                    displaySearchField();
306
                    
307
                    final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
308
                    searchTextField.setText(String.valueOf(stroke.getKeyChar()));
309
310
                    e.consume();
311
                }
312
            }
313
        );
314
        if (isEnabled()) {
315
            component.addKeyListener(quickSearchKeyAdapter);
316
        }
317
        // Create a the "multi-event" listener for the text field. Instead of
318
        // adding separate instances of each needed listener, we're using a
319
        // class which implements them all. This approach is used in order 
320
        // to avoid the creation of 4 instances which takes some time
321
        searchFieldListener = new SearchFieldListener();
322
        searchTextField.addKeyListener(searchFieldListener);
323
        searchTextField.addFocusListener(searchFieldListener);
324
        searchTextField.getDocument().addDocumentListener(searchFieldListener);
325
        
326
    }
327
    
328
    private void displaySearchField() {
329
        if (searchPanel != null || !isEnabled()) {
330
            return;
331
        }
332
        searchTextField.setOriginalFocusOwner();
333
        searchTextField.setFont(component.getFont());
334
        searchPanel = new SearchPanel();
335
        final JLabel lbl;
336
        if (popupMenu != null) {
337
            lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND_WITH_MENU, false));
338
            lbl.addMouseListener(new MouseAdapter() {
339
                @Override
340
                public void mousePressed(MouseEvent e) {
341
                    if (e != null && !SwingUtilities.isLeftMouseButton(e)) {
342
                        return;
343
                    }
344
                    JPopupMenu pm = popupMenu.getPopupMenu();
345
                    pm.show(lbl, 0, lbl.getHeight() - 1);
346
                }
347
            });
348
        } else {
349
            lbl = new JLabel(org.openide.util.ImageUtilities.loadImageIcon(ICON_FIND, false));
350
        }
351
        if (asynchronous) {
352
            animationTimer = new AnimationTimer(lbl, lbl.getIcon());
353
        } else {
354
            animationTimer = null;
355
        }
356
        searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS));
357
        searchPanel.add(lbl);
358
        searchPanel.add(searchTextField);
359
        lbl.setLabelFor(searchTextField);
360
        searchTextField.setColumns(10);
361
        searchTextField.setMaximumSize(searchTextField.getPreferredSize());
362
        searchTextField.putClientProperty("JTextField.variant", "search"); //NOI18N
363
        lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
364
        if (constraints == null) {
365
            component.add(searchPanel);
366
        } else {
367
            component.add(searchPanel, constraints);
368
        }
369
        component.invalidate();
370
        component.revalidate();
371
        component.repaint();
372
        searchTextField.requestFocus();
373
    }
374
    
375
    private void removeSearchField() {
376
        if (searchPanel == null) {
377
            return;
378
        }
379
        if (animationTimer != null) {
380
            animationTimer.stopProgressAnimation();
381
        }
382
        Component sp = searchPanel;
383
        searchPanel = null;
384
        component.remove(sp);
385
        component.invalidate();
386
        component.revalidate();
387
        component.repaint();
388
    }
389
    
390
    /** Accessed from test. */
391
    JTextField getSearchField() {
392
        return searchTextField;
393
    }
394
    
395
    /**
396
     * Utility method, that finds a greatest common prefix of two supplied
397
     * strings.
398
     * 
399
     * @param str1 The first string
400
     * @param str2 The second string
401
     * @param ignoreCase Whether to ignore case in the comparisons
402
     * @return The greatest common prefix of the two strings.
403
     */
404
    public static String findMaxPrefix(String str1, String str2, boolean ignoreCase) {
405
        int n1 = str1.length();
406
        int n2 = str2.length();
407
        int i = 0;
408
        if (ignoreCase) {
409
            for ( ; i < n1 && i < n2; i++) {
410
                char c1 = Character.toUpperCase(str1.charAt(i));
411
                char c2 = Character.toUpperCase(str2.charAt(i));
412
                if (c1 != c2) {
413
                    break;
414
                }
415
            }
416
        } else {
417
            for ( ; i < n1 && i < n2; i++) {
418
                char c1 = str1.charAt(i);
419
                char c2 = str2.charAt(i);
420
                if (c1 != c2) {
421
                    break;
422
                }
423
            }
424
        }
425
        return str1.substring(0, i);
426
    }
427
    
428
    private final static class AnimationTimer {
429
        
430
        private final JLabel jLabel;
431
        private final Icon findIcon;
432
        private final Timer animationTimer;
433
        
434
        public AnimationTimer(final JLabel jLabel, Icon findIcon) {
435
            this.jLabel = jLabel;
436
            this.findIcon = findIcon;
437
            animationTimer = new Timer(100, new ActionListener() {
438
439
                ImageIcon icons[];
440
                int index = 0;
441
442
                @Override
443
                public void actionPerformed(ActionEvent e) {
444
                    if (icons == null) {
445
                        icons = new ImageIcon[8];
446
                        for (int i = 0; i < 8; i++) {
447
                            icons[i] = ImageUtilities.loadImageIcon("org/openide/awt/resources/quicksearch/progress_" + i + ".png", false);  //NOI18N
448
                        }
449
                    }
450
                    jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 6));
451
                    jLabel.setIcon(icons[index]);
452
                    //mac os x
453
                    jLabel.repaint();
454
455
                    index = (index + 1) % 8;
456
                }
457
            });
458
        }
459
        
460
        public void startProgressAnimation() {
461
            if (animationTimer != null && !animationTimer.isRunning()) {
462
                animationTimer.start();
463
            }
464
        }
465
466
        public void stopProgressAnimation() {
467
            if (animationTimer != null && animationTimer.isRunning()) {
468
                animationTimer.stop();
469
                jLabel.setIcon(findIcon);
470
                jLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
471
            }
472
        }
473
474
    }
475
476
    private class LazyFire implements Runnable {
477
        
478
        private final QS_FIRE fire;
479
        //private final QuickSearchListener[] qsls;
480
        private final String searchText;
481
        private final boolean forward;
482
        private final DataContentHandlerFactory newPrefixSetter;
483
        
484
        LazyFire(QS_FIRE fire, String searchText) {
485
            this(fire, searchText, true, null);
486
        }
487
        
488
        LazyFire(QS_FIRE fire, boolean forward) {
489
            this(fire, null, forward);
490
        }
491
        
492
        LazyFire(QS_FIRE fire, String searchText, boolean forward) {
493
            this(fire, searchText, forward, null);
494
        }
495
        
496
        LazyFire(QS_FIRE fire, String searchText,
497
                 DataContentHandlerFactory newPrefixSetter) {
498
            this(fire, searchText, true, newPrefixSetter);
499
        }
500
        
501
        LazyFire(QS_FIRE fire, String searchText, boolean forward,
502
                 DataContentHandlerFactory newPrefixSetter) {
503
            this.fire = fire;
504
            //this.qsls = qsls;
505
            this.searchText = searchText;
506
            this.forward = forward;
507
            this.newPrefixSetter = newPrefixSetter;
508
            animationTimer.startProgressAnimation();
509
        }
510
511
        @Override
512
        public void run() {
513
            try {
514
            switch (fire) {
515
                case UPDATE:    callback.quickSearchUpdate(searchText);//fireQuickSearchUpdate(qsls, searchText);
516
                                break;
517
                case NEXT:      callback.showNextSelection(forward);//fireShowNextSelection(qsls, forward);
518
                                break;
519
                case MAX:       String mp = callback.findMaxPrefix(searchText);//String mp = findMaxPrefix(qsls, searchText);
520
                                newPrefixSetter.createDataContentHandler(mp);
521
                                break;
522
            }
523
            } finally {
524
                animationTimer.stopProgressAnimation();
525
            }
526
        }
527
    }
528
    
529
    private static class SearchPanel extends JPanel {
530
        
531
        public static final boolean isAquaLaF =
532
                "Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N
533
    
534
        public SearchPanel() {
535
            if (isAquaLaF) {
536
                setBorder(BorderFactory.createEmptyBorder(9,6,8,2));
537
            } else {
538
                setBorder(BorderFactory.createEmptyBorder(2,6,2,2));
539
            }
540
            setOpaque(true);
541
        }
542
543
        @Override
544
        protected void paintComponent(Graphics g) {
545
            if (isAquaLaF && g instanceof Graphics2D) {
546
                Graphics2D g2d = (Graphics2D) g;
547
                g2d.setPaint(new GradientPaint(0, 0, UIManager.getColor("NbExplorerView.quicksearch.background.top"), //NOI18N
548
                        0, getHeight(), UIManager.getColor("NbExplorerView.quicksearch.background.bottom")));//NOI18N
549
                g2d.fillRect(0, 0, getWidth(), getHeight());
550
                g2d.setColor(UIManager.getColor("NbExplorerView.quicksearch.border")); //NOI18N
551
                g2d.drawLine(0, 0, getWidth(), 0);
552
            } else {
553
                super.paintComponent(g);
554
            }
555
        }
556
    }
557
558
    /** searchTextField manages focus because it handles VK_ESCAPE key */
559
    private class SearchTextField extends JTextField {
560
        
561
        private WeakReference<Component> originalFocusOwner = new WeakReference<Component>(null);
562
        
563
        public SearchTextField() {
564
        }
565
        
566
        void setOriginalFocusOwner() {
567
            Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
568
            if (focusOwner != null && component.isAncestorOf(focusOwner)) {
569
                originalFocusOwner = new WeakReference<Component>(focusOwner);
570
            } else {
571
                originalFocusOwner = new WeakReference<Component>(component);
572
            }
573
        }
574
        
575
        void requestOriginalFocusOwner() {
576
            SwingUtilities.invokeLater(
577
                new Runnable() {
578
                    //additional bugfix - do focus change later or removing
579
                    //the component while it's focused will cause focus to
580
                    //get transferred to the next component in the
581
                    //parent focusTraversalPolicy *after* our request
582
                    //focus completes, so focus goes into a black hole - Tim
583
                    @Override
584
                    public void run() {
585
                        Component fo = originalFocusOwner.get();
586
                        if (fo != null) {
587
                            fo.requestFocusInWindow();
588
                        }
589
                    }
590
                }
591
            );
592
        }
593
        
594
        @Override
595
        public boolean isManagingFocus() {
596
            return true;
597
        }
598
599
        @Override
600
        public void processKeyEvent(KeyEvent ke) {
601
            //override the default handling so that
602
            //the parent will never receive the escape key and
603
            //close a modal dialog
604
            if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) {
605
                removeSearchField();
606
                ke.consume();
607
                // bugfix #32909, reqest focus when search field is removed
608
                requestOriginalFocusOwner();
609
                //fireQuickSearchCanceled();
610
                callback.quickSearchCanceled();
611
            } else {
612
                super.processKeyEvent(ke);
613
            }
614
        }
615
    };
616
    
617
    private class SearchFieldListener extends KeyAdapter implements DocumentListener, FocusListener {
618
        
619
        private boolean ignoreEvents;
620
621
        SearchFieldListener() {
622
        }
623
624
        @Override
625
        public void changedUpdate(DocumentEvent e) {
626
            if (ignoreEvents) return;
627
            searchForNode();
628
        }
629
630
        @Override
631
        public void insertUpdate(DocumentEvent e) {
632
            if (ignoreEvents) return;
633
            searchForNode();
634
        }
635
636
        @Override
637
        public void removeUpdate(DocumentEvent e) {
638
            if (ignoreEvents) return;
639
            searchForNode();
640
        }
641
642
        @Override
643
        public void keyPressed(KeyEvent e) {
644
            int keyCode = e.getKeyCode();
645
646
            if (keyCode == KeyEvent.VK_ESCAPE) {
647
                removeSearchField();
648
                searchTextField.requestOriginalFocusOwner();
649
                //fireQuickSearchCanceled();
650
                callback.quickSearchCanceled();
651
                e.consume();
652
            } else if (keyCode == KeyEvent.VK_UP || (keyCode == KeyEvent.VK_F3 && e.isShiftDown())) {
653
                fireShowNextSelection(false);
654
                // Stop processing the event here. Otherwise it's dispatched
655
                // to the tree too (which scrolls)
656
                e.consume();
657
            } else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_F3) {
658
                fireShowNextSelection(true);
659
                // Stop processing the event here. Otherwise it's dispatched
660
                // to the tree too (which scrolls)
661
                e.consume();
662
            } else if (keyCode == KeyEvent.VK_TAB) {
663
                findMaxPrefix(searchTextField.getText(), new DataContentHandlerFactory() {
664
                    @Override
665
                    public DataContentHandler createDataContentHandler(final String maxPrefix) {
666
                        if (!SwingUtilities.isEventDispatchThread()) {
667
                            SwingUtilities.invokeLater(new Runnable() {
668
                                @Override
669
                                public void run() {
670
                                    createDataContentHandler(maxPrefix);
671
                                }
672
                            });
673
                            return null;
674
                        }
675
                        ignoreEvents = true;
676
                        try {
677
                            searchTextField.setText(maxPrefix);
678
                        } finally {
679
                            ignoreEvents = false;
680
                        }
681
                        return null;
682
                    }
683
                });
684
685
                e.consume();
686
            } else if (keyCode == KeyEvent.VK_ENTER) {
687
                removeSearchField();
688
                //fireQuickSearchConfirmed();
689
                callback.quickSearchConfirmed();
690
691
                component.requestFocusInWindow();
692
                e.consume();
693
            }
694
        }
695
696
        /** Searches for a node in the tree. */
697
        private void searchForNode() {
698
            String text = searchTextField.getText();
699
            fireQuickSearchUpdate(text);
700
        }
701
702
        @Override
703
        public void focusGained(FocusEvent e) {
704
            if (e.getSource() == searchTextField) {
705
                // make sure nothing is selected
706
                int n = searchTextField.getText().length();
707
                searchTextField.select(n, n);
708
            }
709
        }
710
711
        @Override
712
        public void focusLost(FocusEvent e) {
713
            if (e.isTemporary()) return ;
714
            Component oppositeComponent = e.getOppositeComponent();
715
            if (e.getSource() != searchTextField) {
716
                ((Component) e.getSource()).removeFocusListener(this);
717
            }
718
            if (oppositeComponent instanceof JMenuItem || oppositeComponent instanceof JPopupMenu) {
719
                oppositeComponent.addFocusListener(this);
720
                return ;
721
            }
722
            if (oppositeComponent == searchTextField) {
723
                return ;
724
            }
725
            if (searchPanel != null) {
726
                removeSearchField();
727
                //fireQuickSearchConfirmed();
728
                callback.quickSearchConfirmed();
729
            }
730
        }
731
    }
732
    
733
    /**
734
     * Call back interface, that is notified with the submissions to the quick search field.
735
     * 
736
     * @author Martin Entlicher
737
     * @since 7.43
738
     */
739
    public static interface Callback {
740
        
741
        /**
742
         * Called with an updated search text.
743
         * When {@link #isAsynchronous()} is <code>false</code>
744
         * it's called in EQ thread, otherwise, it's called in a background thread.
745
         * The client should update the visual representation of the search results
746
         *  and then return.<p>
747
         * This method is called to initiate and update the search process.
748
         * @param searchText The new text to search for.
749
         */
750
        void quickSearchUpdate(String searchText);
751
752
        /**
753
         * Called to select a next occurrence of the search result.
754
         * When {@link #isAsynchronous()} is <code>false</code>
755
         * it's called in EQ thread, otherwise, it's called in a background thread.
756
         * The client should update the visual representation of the search results
757
         * and then return.<p>
758
         * @param forward The direction of the next search result.
759
         *                <code>true</code> for forward direction,
760
         *                <code>false</code> for backward direction.
761
         */
762
        void showNextSelection(boolean forward);
763
764
        /**
765
         * Find the maximum prefix among the search results, that starts with the provided string.
766
         * This method is called when user press TAB in the search field, to auto-complete
767
         * the maximum prefix.
768
         * When {@link #isAsynchronous()} is <code>false</code>
769
         * it's called in EQ thread, otherwise, it's called in a background thread.
770
         * Utility method {@link QuickSearch#findMaxPrefix(java.lang.String, java.lang.String, boolean)}
771
         * can be used by the implementation.
772
         * @param prefix The prefix to start with
773
         * @return The maximum prefix.
774
         */
775
        String findMaxPrefix(String prefix);
776
777
        /**
778
         * Called when the quick search is confirmed by the user.
779
         * This method is called in EQ thread always.
780
         */
781
        void quickSearchConfirmed();
782
783
        /**
784
         * Called when the quick search is canceled by the user.
785
         * This method is called in EQ thread always.
786
         */
787
        void quickSearchCanceled();
788
789
    }
790
791
}
(-)openide.awt/test/unit/src/org/openide/awt/QuickSearchTest.java (+699 lines)
Added 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, new DummyCallback());
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, new DummyCallback());
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, new DummyCallback());
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, new DummyCallback());
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
        final String[] searchTextPtr = new String[] { null };
204
        final Boolean[] biasPtr = new Boolean[] { null };
205
        final boolean[] confirmedPtr = new boolean[] { false };
206
        final boolean[] canceledPtr = new boolean[] { false };
207
        QuickSearch.Callback qsc = new QuickSearch.Callback() {
208
            
209
            @Override
210
            public boolean asynchronous() {
211
                return false;
212
            }
213
214
            @Override
215
            public void quickSearchUpdate(String searchText) {
216
                assertTrue(SwingUtilities.isEventDispatchThread());
217
                searchTextPtr[0] = searchText;
218
            }
219
220
            @Override
221
            public void showNextSelection(boolean forward) {
222
                assertTrue(SwingUtilities.isEventDispatchThread());
223
                biasPtr[0] = forward;
224
            }
225
226
            @Override
227
            public String findMaxPrefix(String prefix) {
228
                assertTrue(SwingUtilities.isEventDispatchThread());
229
                return prefix + "endPrefix";
230
            }
231
232
            @Override
233
            public void quickSearchConfirmed() {
234
                assertTrue(SwingUtilities.isEventDispatchThread());
235
                confirmedPtr[0] = true;
236
            }
237
238
            @Override
239
            public void quickSearchCanceled() {
240
                assertTrue(SwingUtilities.isEventDispatchThread());
241
                canceledPtr[0] = true;
242
            }
243
244
        };
245
        QuickSearch qs = QuickSearch.attach(component, constraints, qsc);
246
        component.addNotify();
247
        // Test that a key event passed to the component triggers the quick search:
248
        try {
249
            Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class);
250
            setGlobalFocusOwner.setAccessible(true);
251
            setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), component);
252
        } catch (Exception ex) {
253
            ex.printStackTrace();
254
            throw new AssertionError(ex);
255
        }
256
        KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
257
        component.dispatchEvent(ke);
258
        assertEquals("A", qs.getSearchField().getText());
259
        assertEquals("A", searchTextPtr[0]);
260
        assertNull(biasPtr[0]);
261
        
262
        // Test that further key events passed to the quick search field trigger the quick search listener:
263
        qs.getSearchField().setCaretPosition(1);
264
        try {
265
            Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class);
266
            setGlobalFocusOwner.setAccessible(true);
267
            setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), qs.getSearchField());
268
        } catch (Exception ex) {
269
            ex.printStackTrace();
270
            throw new AssertionError(ex);
271
        }
272
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'b');
273
        qs.getSearchField().dispatchEvent(ke);
274
        assertEquals("Ab", searchTextPtr[0]);
275
        
276
        // Test the up/down keys resulting to selection navigation:
277
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_UP, (char) KeyEvent.VK_UP);
278
        qs.getSearchField().dispatchEvent(ke);
279
        assertEquals(Boolean.FALSE, biasPtr[0]);
280
        
281
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_DOWN, (char) KeyEvent.VK_DOWN);
282
        qs.getSearchField().dispatchEvent(ke);
283
        assertEquals(Boolean.TRUE, biasPtr[0]);
284
        
285
        // Test that tab adds max prefix:
286
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_TAB, '\t');
287
        qs.getSearchField().dispatchEvent(ke);
288
        assertEquals("AbendPrefix", qs.getSearchField().getText());
289
        
290
        /*
291
        // Test that we get no events when quick search listener is detached:
292
        qs.removeQuickSearchListener(qsl);
293
        qs.getSearchField().setCaretPosition(2);
294
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'c');
295
        qs.getSearchField().dispatchEvent(ke);
296
        assertEquals("AbcendPrefix", qs.getSearchField().getText());
297
        assertEquals("Ab", searchTextPtr[0]);
298
        qs.addQuickSearchListener(qsl);
299
        */
300
        
301
        // Test the quick search confirmation on Enter key:
302
        assertFalse(confirmedPtr[0]);
303
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ENTER, '\n');
304
        qs.getSearchField().dispatchEvent(ke);
305
        assertTrue(confirmedPtr[0]);
306
        
307
        // Test the quick search cancel on ESC key:
308
        ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
309
        component.dispatchEvent(ke);
310
        assertEquals("A", searchTextPtr[0]);
311
        assertFalse(canceledPtr[0]);
312
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ESCAPE, (char) 27);
313
        qs.getSearchField().dispatchEvent(ke);
314
        assertTrue(canceledPtr[0]);
315
    }
316
    
317
    enum sync { W, N } // Wait, Notify
318
319
    /**
320
     * Test of asynchronous calls, of class QuickSearch.
321
     */
322
    @Test
323
    public void testAsynchronous() {
324
        final TestComponent[] componentPtr = new TestComponent[] { null };
325
        final String[] searchTextPtr = new String[] { null };
326
        final Boolean[] biasPtr = new Boolean[] { null };
327
        final Object findMaxPrefixLock = new Object();
328
        final boolean[] confirmedPtr = new boolean[] { false };
329
        final boolean[] canceledPtr = new boolean[] { false };
330
        final boolean[] asynchronousPtr = new boolean[] { false };
331
        final sync[] syncPtr = new sync[] { null };
332
        final QuickSearch.Callback qsc = new QuickSearch.Callback() {
333
334
            @Override
335
            public boolean asynchronous() {
336
                return asynchronousPtr[0];
337
            }
338
            
339
            @Override
340
            public void quickSearchUpdate(String searchText) {
341
                assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread());
342
                synchronized(searchTextPtr) {
343
                    if (syncPtr[0] == null) {
344
                        syncPtr[0] = sync.W;
345
                        // Wait for the notification first
346
                        try { searchTextPtr.wait(); } catch (InterruptedException iex) {}
347
                    }
348
                    searchTextPtr[0] = searchText;
349
                    searchTextPtr.notifyAll();
350
                    syncPtr[0] = null;
351
                }
352
            }
353
354
            @Override
355
            public void showNextSelection(boolean forward) {
356
                assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread());
357
                synchronized(biasPtr) {
358
                    if (syncPtr[0] == null) {
359
                        syncPtr[0] = sync.W;
360
                        // Wait for the notification first
361
                        try { biasPtr.wait(); } catch (InterruptedException iex) {}
362
                    }
363
                    biasPtr[0] = forward;
364
                    biasPtr.notifyAll();
365
                    syncPtr[0] = null;
366
                }
367
            }
368
369
            @Override
370
            public String findMaxPrefix(String prefix) {
371
                assertTrue(asynchronousPtr[0] != SwingUtilities.isEventDispatchThread());
372
                synchronized(findMaxPrefixLock) {
373
                    if (syncPtr[0] == null) {
374
                        syncPtr[0] = sync.W;
375
                        // Wait for the notification first
376
                        try { findMaxPrefixLock.wait(); } catch (InterruptedException iex) {}
377
                    }
378
                    prefix = prefix + "endPrefix";
379
                    findMaxPrefixLock.notifyAll();
380
                    syncPtr[0] = null;
381
                }
382
                return prefix;
383
            }
384
385
            @Override
386
            public void quickSearchConfirmed() {
387
                assertTrue(SwingUtilities.isEventDispatchThread());
388
                confirmedPtr[0] = true;
389
            }
390
391
            @Override
392
            public void quickSearchCanceled() {
393
                assertTrue(SwingUtilities.isEventDispatchThread());
394
                canceledPtr[0] = true;
395
            }
396
397
        };
398
        final QuickSearch[] qsPtr = new QuickSearch[] { null };
399
        try {
400
            SwingUtilities.invokeAndWait(new Runnable() {
401
                @Override
402
                public void run() {
403
                    componentPtr[0] = new TestComponent();
404
                    qsPtr[0] = QuickSearch.attach(componentPtr[0], null, qsc);
405
                    componentPtr[0].addNotify();
406
                }
407
            });
408
        } catch (InterruptedException iex) {
409
            fail("interrupted.");
410
        } catch (InvocationTargetException itex) {
411
            Throwable cause = itex.getCause();
412
            if (cause instanceof AssertionError) {
413
                throw (AssertionError) cause;
414
            }
415
            itex.getCause().printStackTrace();
416
            throw new AssertionError(cause);
417
        }
418
        assertFalse(qsc.asynchronous());
419
        asynchronousPtr[0] = true;
420
        assertTrue(qsc.asynchronous());
421
        
422
        // Test that a key event passed to the component triggers the asynchronous quick search:
423
        try {
424
            SwingUtilities.invokeAndWait(new Runnable() {
425
                @Override
426
                public void run() {
427
                    try {
428
                        Method setGlobalFocusOwner = KeyboardFocusManager.class.getDeclaredMethod("setGlobalFocusOwner", Component.class);
429
                        setGlobalFocusOwner.setAccessible(true);
430
                        setGlobalFocusOwner.invoke(KeyboardFocusManager.getCurrentKeyboardFocusManager(), componentPtr[0]);
431
                    } catch (Exception ex) {
432
                        ex.printStackTrace();
433
                        throw new AssertionError(ex);
434
                    }
435
                    KeyEvent ke = new KeyEvent(componentPtr[0], KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
436
                    componentPtr[0].dispatchEvent(ke);
437
                }
438
            });
439
        } catch (InterruptedException iex) {
440
            fail("interrupted.");
441
        } catch (InvocationTargetException itex) {
442
            Throwable cause = itex.getCause();
443
            if (cause instanceof AssertionError) {
444
                throw (AssertionError) cause;
445
            }
446
            itex.getCause().printStackTrace();
447
            throw new AssertionError(cause);
448
        }
449
        synchronized(searchTextPtr) {
450
            assertNull(searchTextPtr[0]);
451
            syncPtr[0] = sync.N;
452
            searchTextPtr.notifyAll();
453
            // Wait to set the value
454
            try { searchTextPtr.wait(); } catch (InterruptedException iex) {}
455
            assertEquals("A", searchTextPtr[0]);
456
        }
457
        
458
        // Test the up/down keys resulting to asynchronous selection navigation:
459
        try {
460
            SwingUtilities.invokeAndWait(new Runnable() {
461
                @Override
462
                public void run() {
463
                    KeyEvent ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_UP, (char) KeyEvent.VK_UP);
464
                    qsPtr[0].getSearchField().dispatchEvent(ke);
465
466
                    ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_DOWN, (char) KeyEvent.VK_DOWN);
467
                    qsPtr[0].getSearchField().dispatchEvent(ke);
468
                }
469
            });
470
        } catch (InterruptedException iex) {
471
            fail("interrupted.");
472
        } catch (InvocationTargetException itex) {
473
            Throwable cause = itex.getCause();
474
            if (cause instanceof AssertionError) {
475
                throw (AssertionError) cause;
476
            }
477
            itex.getCause().printStackTrace();
478
            throw new AssertionError(cause);
479
        }
480
        synchronized(biasPtr) {
481
            assertNull(biasPtr[0]);
482
            syncPtr[0] = sync.N;
483
            biasPtr.notifyAll();
484
            // Wait to set the value
485
            try { biasPtr.wait(); } catch (InterruptedException iex) {}
486
            assertEquals(Boolean.FALSE, biasPtr[0]);
487
        }
488
        synchronized(biasPtr) {
489
            assertEquals(Boolean.FALSE, biasPtr[0]);
490
            syncPtr[0] = sync.N;
491
            biasPtr.notifyAll();
492
            // Wait to set the value
493
            try { biasPtr.wait(); } catch (InterruptedException iex) {}
494
            assertEquals(Boolean.TRUE, biasPtr[0]);
495
        }
496
        
497
        // Test that tab adds max prefix asynchronously:
498
        try {
499
            SwingUtilities.invokeAndWait(new Runnable() {
500
                @Override
501
                public void run() {
502
                    KeyEvent ke = new KeyEvent(qsPtr[0].getSearchField(), KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_TAB, '\t');
503
                    qsPtr[0].getSearchField().dispatchEvent(ke);
504
                }
505
            });
506
        } catch (InterruptedException iex) {
507
            fail("interrupted.");
508
        } catch (InvocationTargetException itex) {
509
            Throwable cause = itex.getCause();
510
            if (cause instanceof AssertionError) {
511
                throw (AssertionError) cause;
512
            }
513
            itex.getCause().printStackTrace();
514
            throw new AssertionError(cause);
515
        }
516
        synchronized(findMaxPrefixLock) {
517
            assertEquals("A", qsPtr[0].getSearchField().getText());
518
            syncPtr[0] = sync.N;
519
            findMaxPrefixLock.notifyAll();
520
            // Wait to set the value
521
            try { findMaxPrefixLock.wait(); } catch (InterruptedException iex) {}
522
            // Can not test it immediatelly, the text is updated in AWT
523
            // assertEquals("AendPrefix", qsPtr[0].getSearchField().getText());
524
        }
525
        try { Thread.sleep(200); } catch (InterruptedException iex) {}
526
        try {
527
            SwingUtilities.invokeAndWait(new Runnable() {
528
                @Override
529
                public void run() {
530
                    assertEquals("AendPrefix", qsPtr[0].getSearchField().getText());
531
                }
532
            });
533
        } catch (InterruptedException iex) {
534
            fail("interrupted.");
535
        } catch (InvocationTargetException itex) {
536
            Throwable cause = itex.getCause();
537
            if (cause instanceof AssertionError) {
538
                throw (AssertionError) cause;
539
            }
540
            itex.getCause().printStackTrace();
541
            throw new AssertionError(cause);
542
        }
543
    }
544
    
545
    /**
546
     * Test of processKeyEvent method, of class QuickSearch.
547
     */
548
    @Test
549
    public void testProcessKeyEvent() {
550
        TestComponent component = new TestComponent();
551
        Object constraints = null;
552
        final String[] searchTextPtr = new String[] { null };
553
        final Boolean[] biasPtr = new Boolean[] { null };
554
        final boolean[] confirmedPtr = new boolean[] { false };
555
        final boolean[] canceledPtr = new boolean[] { false };
556
        final QuickSearch.Callback qsc = new QuickSearch.Callback() {
557
558
            @Override
559
            public boolean asynchronous() {
560
                return false;
561
            }
562
            
563
            @Override
564
            public void quickSearchUpdate(String searchText) {
565
                searchTextPtr[0] = searchText;
566
            }
567
568
            @Override
569
            public void showNextSelection(boolean forward) {
570
                biasPtr[0] = forward;
571
            }
572
573
            @Override
574
            public String findMaxPrefix(String prefix) {
575
                return prefix + "endPrefix";
576
            }
577
578
            @Override
579
            public void quickSearchConfirmed() {
580
                confirmedPtr[0] = true;
581
            }
582
583
            @Override
584
            public void quickSearchCanceled() {
585
                canceledPtr[0] = true;
586
            }
587
        };
588
        QuickSearch qs = QuickSearch.attach(component, constraints, qsc);
589
        KeyEvent ke = new KeyEvent(component, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'A');
590
        qs.processKeyEvent(ke);
591
        assertEquals("A", qs.getSearchField().getText());
592
        assertEquals("A", searchTextPtr[0]);
593
        assertNull(biasPtr[0]);
594
        
595
        ke = new KeyEvent(qs.getSearchField(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'b');
596
        qs.processKeyEvent(ke);
597
        assertEquals("Ab", qs.getSearchField().getText());
598
        assertEquals("Ab", searchTextPtr[0]);
599
    }
600
601
    /**
602
     * Test of findMaxCommonSubstring method, of class QuickSearch.
603
     */
604
    @Test
605
    public void testFindMaxCommonSubstring() {
606
        System.out.println("findMaxCommonSubstring");
607
        String str1 = "annotation";
608
        String str2 = "antenna";
609
        boolean ignoreCase = false;
610
        String expResult = "an";
611
        String result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase);
612
        assertEquals(expResult, result);
613
        str1 = "Annotation";
614
        expResult = "";
615
        result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase);
616
        assertEquals(expResult, result);
617
        str1 = "AbCdEf";
618
        str2 = "AbCxxx";
619
        expResult = "AbC";
620
        result = QuickSearch.findMaxPrefix(str1, str2, ignoreCase);
621
        assertEquals(expResult, result);
622
    }
623
    
624
    private static final class TestComponent extends JComponent {
625
        
626
        List<KeyListener> addedKeyListeners = new ArrayList<KeyListener>();
627
        Component added;
628
        Object constraints;
629
        
630
        public TestComponent() {
631
            new JFrame().add(this); // To have a parent
632
        }
633
634
        @Override
635
        public boolean isShowing() {
636
            return true;
637
        }
638
        
639
        @Override
640
        public Component add(Component comp) {
641
            this.added = comp;
642
            return super.add(comp);
643
        }
644
645
        @Override
646
        public void add(Component comp, Object constraints) {
647
            this.added = comp;
648
            this.constraints = constraints;
649
            super.add(comp, constraints);
650
        }
651
652
        @Override
653
        public void remove(Component comp) {
654
            if (comp == this.added) {
655
                this.added = null;
656
            }
657
            super.remove(comp);
658
        }
659
        
660
        @Override
661
        public synchronized void addKeyListener(KeyListener l) {
662
            addedKeyListeners.add(l);
663
            super.addKeyListener(l);
664
        }
665
666
        @Override
667
        public synchronized void removeKeyListener(KeyListener l) {
668
            addedKeyListeners.remove(l);
669
            super.removeKeyListener(l);
670
        }
671
        
672
    }
673
    
674
    private static final class DummyCallback implements QuickSearch.Callback {
675
676
        @Override
677
        public boolean asynchronous() {
678
            return false;
679
        }
680
681
        @Override
682
        public void quickSearchUpdate(String searchText) {}
683
684
        @Override
685
        public void showNextSelection(boolean forward) {}
686
687
        @Override
688
        public String findMaxPrefix(String prefix) {
689
            return prefix;
690
        }
691
692
        @Override
693
        public void quickSearchConfirmed() {}
694
695
        @Override
696
        public void quickSearchCanceled() {}
697
        
698
    }
699
}

Return to bug 208794
By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2014, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo