001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.xml.xam.ui.search;
043:
044: import java.awt.Color;
045: import java.awt.event.ActionEvent;
046: import java.awt.event.ActionListener;
047: import java.awt.event.FocusEvent;
048: import java.awt.event.FocusListener;
049: import java.awt.event.KeyEvent;
050: import java.awt.event.KeyListener;
051: import java.awt.event.MouseEvent;
052: import java.awt.event.MouseListener;
053: import java.util.Collection;
054: import java.util.Collections;
055: import java.util.HashMap;
056: import java.util.Iterator;
057: import java.util.List;
058: import java.util.Map;
059: import javax.swing.AbstractButton;
060: import javax.swing.ButtonModel;
061: import javax.swing.JCheckBoxMenuItem;
062: import javax.swing.JMenuItem;
063: import javax.swing.JPanel;
064: import javax.swing.JRadioButtonMenuItem;
065: import javax.swing.JSeparator;
066: import javax.swing.UIManager;
067: import javax.swing.event.DocumentEvent;
068: import javax.swing.event.DocumentListener;
069: import javax.swing.event.EventListenerList;
070: import javax.swing.event.PopupMenuEvent;
071: import javax.swing.event.PopupMenuListener;
072: import org.openide.util.NbBundle;
073: import org.openide.util.WeakListeners;
074:
075: /**
076: * Interface for searching an XML document.
077: *
078: * @author Nathan Fiedler
079: */
080: public class SearchFieldPanel extends JPanel implements ActionListener,
081: DocumentListener, FocusListener, KeyListener, MouseListener,
082: PopupMenuListener {
083: /** Color used to indicate no match. */
084: private static final Color MATCH_FAILED_COLOR = new Color(255, 102,
085: 102);
086: /** silence compiler warnings */
087: static final long serialVersionUID = 1L;
088: /** A mapping of action command name to SearchProvider instances. */
089: private Map<String, SearchProvider> buttonProviderMap;
090: /** If true, the text field is expecting to get input from the user,
091: * otherwise it is displaying the search type label. */
092: private boolean expectingInput;
093: /** Starts the search from the selected component. */
094: private JCheckBoxMenuItem fromSelectedMenuItem;
095: /** Indicates if search phrase is a regular expression. */
096: private JCheckBoxMenuItem regexMenuItem;
097: /** List of search listeners. */
098: private EventListenerList searchListeners = new EventListenerList();
099: /** Last search string. */
100: private String lastSearchString;
101:
102: /**
103: * Creates new form SearchFieldPanel.
104: */
105: public SearchFieldPanel() {
106: initComponents();
107: typesButton.addActionListener(this );
108: searchTextField.addKeyListener(this );
109: searchTextField.addActionListener(this );
110: searchTextField.addFocusListener(this );
111: searchTextField.addMouseListener(this );
112: searchTextField.getDocument().addDocumentListener(this );
113: typesPopupMenu.addPopupMenuListener(this );
114: buttonProviderMap = new HashMap<String, SearchProvider>();
115: }
116:
117: String lastSearchString() {
118: return lastSearchString;
119: }
120:
121: String currentSearchString() {
122: return searchTextField.getText();
123: }
124:
125: public void actionPerformed(ActionEvent e) {
126: Object src = e.getSource();
127: if (src == typesButton) {
128: typesPopupMenu
129: .show(typesButton, 0, typesButton.getHeight());
130: } else if (src == searchTextField) {
131: newSearch();
132: } else if (src instanceof JMenuItem) {
133: if (!expectingInput) {
134: // One of the serach type menu items was selected, need to
135: // update the search text field appropriately.
136: indicateSearchType();
137: } else {
138: // Need to set the text field tooltip appropriately.
139: ButtonModel bm = typeButtonGroup.getSelection();
140: SearchProvider sp = buttonProviderMap.get(bm
141: .getActionCommand());
142: searchTextField
143: .setToolTipText(sp.getInputDescription());
144: }
145: }
146: }
147:
148: void newSearch() {
149: fireSearchEvent(SearchEvent.Type.COMMENCED);
150: // Perform the search.
151: String text = searchTextField.getText();
152: if (text.length() > 0) {
153: ButtonModel model = typeButtonGroup.getSelection();
154: SearchProvider provider = buttonProviderMap.get(model
155: .getActionCommand());
156: boolean selected = fromSelectedMenuItem.isSelected();
157: boolean regex = regexMenuItem.isSelected();
158: Query query = new Query(text, selected, regex);
159: try {
160: List<Object> results = provider.search(query);
161: if (results.isEmpty()) {
162: indicateFailure();
163: } else {
164: clearFailure(false);
165: }
166: lastSearchString = text;
167: fireSearchComplete(results);
168: } catch (SearchException se) {
169: fireSearchFailed(se);
170: }
171: } else {
172: // Notify listeners that the search was empty.
173: List<Object> empty = Collections.emptyList();
174: fireSearchComplete(empty);
175: }
176: }
177:
178: /**
179: * Add the given search listener to this search interface. The listener
180: * will be notified when the user performs a search.
181: *
182: * @param l search listener.
183: */
184: public void addSearchListener(SearchListener l) {
185: searchListeners.add(SearchListener.class, l);
186: }
187:
188: public void changedUpdate(DocumentEvent e) {
189: }
190:
191: /**
192: * Reset the text field colors to their usual defaults.
193: *
194: * @param clear true to erase contents of text field, false to leave as-is.
195: */
196: private void clearFailure(boolean clear) {
197: Color color = UIManager.getDefaults().getColor(
198: "TextField.background");
199: setBackground(color);
200: searchTextField.setBackground(color);
201: color = UIManager.getDefaults()
202: .getColor("TextField.foreground");
203: searchTextField.setForeground(color);
204: if (clear) {
205: searchTextField.setText("");
206: }
207: }
208:
209: /**
210: * Notify the search listeners that the search is complete.
211: *
212: * @return results list of search results.
213: */
214: protected void fireSearchComplete(List<Object> results) {
215: fireSearchEvent(new SearchEvent(this ,
216: SearchEvent.Type.FINISHED, results));
217: }
218:
219: /**
220: * Notify the search listeners of a particular search event type.
221: *
222: * @param type search event type.
223: */
224: private void fireSearchEvent(SearchEvent.Type type) {
225: fireSearchEvent(new SearchEvent(this , type));
226: }
227:
228: /**
229: * Notify the search listeners that the search failed.
230: *
231: * @return error the search exception that occurred.
232: */
233: protected void fireSearchFailed(SearchException error) {
234: fireSearchEvent(new SearchEvent(this , SearchEvent.Type.FAILED,
235: error));
236: }
237:
238: /**
239: * Notify the search listeners of a particular search event type.
240: */
241: private void fireSearchEvent(SearchEvent event) {
242: Object[] listeners = searchListeners.getListenerList();
243: SearchEvent.Type type = event.getType();
244: for (int ii = listeners.length - 2; ii >= 0; ii -= 2) {
245: if (listeners[ii] == SearchListener.class) {
246: type.fireEvent(event,
247: (SearchListener) listeners[ii + 1]);
248: }
249: }
250: }
251:
252: public void focusGained(FocusEvent e) {
253: // Ignore focus changes due to switching between apps.
254: if (e.getSource() == searchTextField && !expectingInput) {
255: // Field is showing search type name, so clear it for user to
256: // enter input, otherwise it was user input, so leave it as-is.
257: clearFailure(true);
258: expectingInput = true;
259: }
260: }
261:
262: public void focusLost(FocusEvent e) {
263: if (e.getSource() == searchTextField
264: && searchTextField.getText().length() == 0) {
265: clearFailure(false);
266: indicateSearchType();
267: }
268: }
269:
270: /**
271: * Indicate that the user's query did not match anything.
272: */
273: private void indicateFailure() {
274: setBackground(MATCH_FAILED_COLOR);
275: searchTextField.setBackground(MATCH_FAILED_COLOR);
276: searchTextField.setForeground(Color.white);
277: }
278:
279: /**
280: * Displays the search provider name in the text field in grey color.
281: */
282: private void indicateSearchType() {
283: // We are no longer expecting input from the user.
284: expectingInput = false;
285: ButtonModel bm = typeButtonGroup.getSelection();
286: if (bm == null) {
287: // No button is selected, most likely because there are no
288: // providers. Just have to wait for them to become available.
289: return;
290: }
291: SearchProvider provider = buttonProviderMap.get(bm
292: .getActionCommand());
293: String name = provider.getDisplayName();
294: Color color = UIManager.getDefaults().getColor(
295: "textInactiveText");
296: searchTextField.setForeground(color);
297: searchTextField.setText(name);
298: searchTextField.setToolTipText(provider.getInputDescription());
299: }
300:
301: public void insertUpdate(DocumentEvent e) {
302: // To ignore the "insert" performed by showing the search type,
303: // check if expectingInput is true or not. This was used to make
304: // the close button enable, but now the close button has moved.
305: }
306:
307: public void keyPressed(KeyEvent e) {
308: }
309:
310: public void keyReleased(KeyEvent e) {
311: if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
312: if (!expectingInput) {
313: clearFailure(true);
314: indicateSearchType();
315: }
316: fireSearchEvent(SearchEvent.Type.DISMISSED);
317: // Send the focus away from the text field.
318: typesButton.requestFocusInWindow();
319: }
320: }
321:
322: public void keyTyped(KeyEvent e) {
323: }
324:
325: public void mouseClicked(MouseEvent e) {
326: }
327:
328: public void mouseEntered(MouseEvent e) {
329: }
330:
331: public void mouseExited(MouseEvent e) {
332: }
333:
334: public void mousePressed(MouseEvent e) {
335: // This handles the case where the user performs a quick-copy
336: // (on X Windows, select some text, then middle-click in a text field)
337: // and the text field is about to receive some text.
338: if (!expectingInput) {
339: clearFailure(true);
340: expectingInput = true;
341: }
342: }
343:
344: public void mouseReleased(MouseEvent e) {
345: }
346:
347: public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
348: if (!expectingInput) {
349: clearFailure(true);
350: }
351: }
352:
353: public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
354: }
355:
356: public void popupMenuCanceled(PopupMenuEvent e) {
357: }
358:
359: /**
360: * Make the search field ready for input from the user.
361: *
362: * @param clear if true, clear the input text field.
363: */
364: public void prepareForInput(boolean clear) {
365: clearFailure(false);
366: searchTextField.requestFocusInWindow();
367: if (clear) {
368: searchTextField.setText("");
369: } else {
370: searchTextField.selectAll();
371: }
372: }
373:
374: /**
375: * Rmove the given search listener from this search interface.
376: *
377: * @param l search listener.
378: */
379: public void removeSearchListener(SearchListener l) {
380: searchListeners.remove(SearchListener.class, l);
381: }
382:
383: public void removeUpdate(DocumentEvent e) {
384: if (e.getDocument().getLength() == 0) {
385: // Remove the error indicator colors.
386: clearFailure(false);
387: }
388: }
389:
390: public void setEnabled(boolean enabled) {
391: super .setEnabled(enabled);
392: typesButton.setEnabled(enabled);
393: searchTextField.setEnabled(enabled);
394: if (!expectingInput) {
395: clearFailure(true);
396: indicateSearchType();
397: }
398: }
399:
400: /**
401: * Set the collection of available SearchProvider implementations.
402: *
403: * @param providers collection of search providers, must be non-empty.
404: */
405: public void setProviders(Collection providers) {
406: if (providers.size() == 0) {
407: throw new IllegalArgumentException(
408: "providers must be non-empty");
409: }
410: buttonProviderMap.clear();
411: typesPopupMenu.removeAll();
412: Iterator iter = providers.iterator();
413: while (iter.hasNext()) {
414: SearchProvider provider = (SearchProvider) iter.next();
415: String name = provider.getDisplayName();
416: JRadioButtonMenuItem item = new JRadioButtonMenuItem(name);
417: item.addActionListener((ActionListener) WeakListeners
418: .create(ActionListener.class, this , item));
419: item.setActionCommand(name);
420: item.setToolTipText(provider.getShortDescription());
421: buttonProviderMap.put(name, provider);
422: typeButtonGroup.add(item);
423: typesPopupMenu.add(item);
424: }
425: // Set the first item selected by default.
426: AbstractButton button = (AbstractButton) typesPopupMenu
427: .getComponent(0);
428: button.setSelected(true);
429: if (!expectingInput) {
430: clearFailure(true);
431: indicateSearchType();
432: }
433:
434: typesPopupMenu.add(new JSeparator());
435:
436: if (fromSelectedMenuItem == null) {
437: fromSelectedMenuItem = new JCheckBoxMenuItem();
438: fromSelectedMenuItem
439: .addActionListener((ActionListener) WeakListeners
440: .create(ActionListener.class, this ,
441: fromSelectedMenuItem));
442: fromSelectedMenuItem.setText(NbBundle.getMessage(
443: SearchFieldPanel.class,
444: "LBL_SearchField_StartFromSelected"));
445: }
446: typesPopupMenu.add(fromSelectedMenuItem);
447:
448: if (regexMenuItem == null) {
449: regexMenuItem = new JCheckBoxMenuItem();
450: regexMenuItem
451: .addActionListener((ActionListener) WeakListeners
452: .create(ActionListener.class, this ,
453: regexMenuItem));
454: regexMenuItem.setText(NbBundle.getMessage(
455: SearchFieldPanel.class,
456: "LBL_SearchField_RegularExpression"));
457: }
458: typesPopupMenu.add(regexMenuItem);
459: }
460:
461: /** This method is called from within the constructor to
462: * initialize the form.
463: * WARNING: Do NOT modify this code. The content of this method is
464: * always regenerated by the Form Editor.
465: */
466: // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:initComponents
467: private void initComponents() {
468: typesPopupMenu = new javax.swing.JPopupMenu();
469: typeButtonGroup = new javax.swing.ButtonGroup();
470: typesButton = new javax.swing.JButton();
471: searchTextField = new javax.swing.JTextField();
472:
473: setBackground(javax.swing.UIManager.getDefaults().getColor(
474: "TextField.background"));
475: setBorder(javax.swing.BorderFactory.createEtchedBorder());
476: typesButton.setBackground(javax.swing.UIManager.getDefaults()
477: .getColor("TextField.background"));
478: typesButton
479: .setIcon(new javax.swing.ImageIcon(
480: getClass()
481: .getResource(
482: "/org/netbeans/modules/xml/xam/ui/search/search_types.png")));
483: typesButton.setToolTipText(java.util.ResourceBundle.getBundle(
484: "org/netbeans/modules/xml/xam/ui/search/Bundle")
485: .getString("HINT_SearchFieldPanel_Types"));
486: typesButton.setBorderPainted(false);
487: typesButton.setContentAreaFilled(false);
488: typesButton
489: .setDisabledIcon(new javax.swing.ImageIcon(
490: getClass()
491: .getResource(
492: "/org/netbeans/modules/xml/xam/ui/search/search_types_disabled.png")));
493: typesButton.setMargin(new java.awt.Insets(0, 0, 0, 0));
494:
495: searchTextField.setColumns(15);
496: searchTextField.setBorder(null);
497:
498: org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(
499: this );
500: this .setLayout(layout);
501: layout
502: .setHorizontalGroup(layout
503: .createParallelGroup(
504: org.jdesktop.layout.GroupLayout.LEADING)
505: .add(
506: layout
507: .createSequentialGroup()
508: .add(typesButton)
509: .addPreferredGap(
510: org.jdesktop.layout.LayoutStyle.RELATED)
511: .add(searchTextField)));
512: layout.setVerticalGroup(layout.createParallelGroup(
513: org.jdesktop.layout.GroupLayout.LEADING).add(
514: typesButton).add(searchTextField,
515: org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 22,
516: Short.MAX_VALUE));
517: }// </editor-fold>//GEN-END:initComponents
518:
519: // Variables declaration - do not modify//GEN-BEGIN:variables
520: private javax.swing.JTextField searchTextField;
521: private javax.swing.ButtonGroup typeButtonGroup;
522: private javax.swing.JButton typesButton;
523: private javax.swing.JPopupMenu typesPopupMenu;
524: // End of variables declaration//GEN-END:variables
525: }
|