001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2008 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-2008 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.search;
043:
044: import java.awt.Component;
045: import java.awt.EventQueue;
046: import java.awt.event.ActionEvent;
047: import java.beans.PropertyChangeEvent;
048: import java.beans.PropertyChangeListener;
049: import java.beans.PropertyChangeSupport;
050: import java.lang.ref.Reference;
051: import java.lang.ref.WeakReference;
052: import java.util.List;
053: import java.util.Map;
054: import java.util.logging.Logger;
055: import javax.swing.Action;
056: import javax.swing.JMenuItem;
057: import javax.swing.event.ChangeEvent;
058: import javax.swing.event.ChangeListener;
059: import org.openide.DialogDisplayer;
060: import org.openide.NotifyDescriptor;
061: import org.openide.util.ContextAwareAction;
062: import org.openide.util.Lookup;
063: import org.openide.util.Mutex;
064: import org.openide.util.actions.CallableSystemAction;
065: import org.openide.util.HelpCtx;
066: import org.openide.util.NbBundle;
067: import org.openide.util.actions.Presenter;
068: import org.openidex.search.SearchType;
069: import static java.util.logging.Level.FINER;
070:
071: /**
072: * Action which searches files in folders, packages and projects.
073: * <p>
074: * This action uses two different mechanisms of enabling/disabling,
075: * depending on whether the action is available in the toolbar or not:
076: * <ul>
077: * <li><u>if the action is in the toolbar:</u><br />
078: * The action is updated (enabled/disabled) continuously.
079: * </li>
080: * <li><u>if the action is <em>not</em> in the toolbar</u><br />
081: * The action state is not updated but it is computed on demand,
082: * i.e. when method <code>isEnabled()</code> is called.
083: * </li>
084: * </ul>
085: * Moreover, the first call of method <code>isEnabled()</code> returns
086: * <code>false</code>, no matter whether some projects are open or not.
087: * This is made so based on the assumption that the first call of
088: * <code>isEnabled()</code> is done during IDE startup as a part of menu
089: * creation. It reduces startup time as it does not force projects
090: * initialization.
091: *
092: * @author Marian Petras
093: */
094: public class FindInFilesAction extends CallableSystemAction implements
095: ContextAwareAction, ChangeListener {
096:
097: static final long serialVersionUID = 4554342565076372611L;
098:
099: private static final Logger LOG = Logger
100: .getLogger("org.netbeans.modules.search.FindAction_state"); //NOI18N
101:
102: /**
103: * name of a shared variable - is this the first call of method
104: * <code>isEnabled()</code>?
105: * Value of this variable is non-<code>null</code> only until method
106: * {@link #isEnabled()} is called for the first time.
107: */
108: private static final String VAR_FIRST_ISENABLED = "first call of isEnabled()"; //NOI18N
109: /**
110: * name of a shared variable - reference to the toolbar presenter
111: */
112: private static final String VAR_TOOLBAR_COMP_REF = "toolbar presenter ref"; //NOI18N
113: /**
114: * name of a shared variable - are we listening on the set of open projects?
115: * It contains <code>Boolean.TRUE</code> if we are listening,
116: * and <code>null</code> if we are not listening.
117: */
118: private static final String VAR_LISTENING = "listening"; //NOI18N
119:
120: /**
121: * name of property "replacing".
122: * Value of the property determines whether the action should offer
123: * replacing of found matching strings (if {@code true}) or not
124: * (if {@code false}). Value {@code true} thus effectively modifies
125: * action "find in files" to "replace in files".
126: */
127: protected static final String REPLACING = "replacing"; //NOI18N
128:
129: @Override
130: protected void initialize() {
131: super .initialize();
132: putValue("noIconInMenu", Boolean.TRUE); // NOI18N
133: putProperty(VAR_FIRST_ISENABLED, Boolean.TRUE);
134:
135: putProperty(REPLACING, Boolean.FALSE, false);
136: }
137:
138: public Action createContextAwareInstance(Lookup lookup) {
139: if (shouldLog(LOG)) {
140: log("createContextAwareInstance(lookup)");
141: }
142: return new LookupSensitive(this , lookup);
143: }
144:
145: public Action createContextAwareInstance(Lookup lookup,
146: boolean searchSelection) {
147: if (shouldLog(LOG)) {
148: log("createContextAwareInstance(lookup, " + searchSelection
149: + ')');
150: }
151: Action result = new LookupSensitive(this , lookup,
152: searchSelection);
153: if (shouldLog(LOG)) {
154: log(" -> " + result);
155: }
156: return result;
157: }
158:
159: @Override
160: public Component getToolbarPresenter() {
161: assert EventQueue.isDispatchThread();
162: if (shouldLog(LOG)) {
163: log("getMenuPresenter()");
164: }
165:
166: Component presenter = getStoredToolbarPresenter();
167: if (putProperty(VAR_LISTENING, Boolean.TRUE) == null) {
168: SearchScopeRegistry.getDefault().addChangeListener(this );
169: putProperty(VAR_FIRST_ISENABLED, null);
170: updateState();
171: }
172: return presenter;
173: }
174:
175: /**
176: * Returns a toolbar presenter.
177: * If the toolbar presenter already exists, returns the existing instance.
178: * If it does not exist, creates a new toolbar presenter, stores
179: * a reference to it to shared variable <code>VAR_TOOLBAR_BTN_REF</code>
180: * and returns the presenter.
181: *
182: * @return existing presenter; or a new presenter if it did not exist
183: */
184: private Component getStoredToolbarPresenter() {
185: assert EventQueue.isDispatchThread();
186: if (shouldLog(LOG)) {
187: log("getStoredToolbarPresenter()");
188: }
189:
190: Object refObj = getProperty(VAR_TOOLBAR_COMP_REF);
191: if (refObj != null) {
192: Reference ref = (Reference) refObj;
193: Object presenterObj = ref.get();
194: if (presenterObj != null) {
195: return (Component) presenterObj;
196: }
197: }
198:
199: Component presenter = super .getToolbarPresenter();
200: putProperty(VAR_TOOLBAR_COMP_REF, new WeakReference<Component>(
201: presenter));
202: return presenter;
203: }
204:
205: /**
206: * Checks whether the stored toolbar presenter exists but does not create
207: * one if it does not exist.
208: *
209: * @return <code>true</code> if the reference to the toolbar presenter
210: * is not <code>null</code> and has not been cleared yet;
211: * <code>false</code> otherwise
212: * @see #getStoredToolbarPresenter
213: */
214: private boolean checkToolbarPresenterExists() {
215: assert EventQueue.isDispatchThread();
216: if (shouldLog(LOG)) {
217: log("checkToolbarPresenterExists()");
218: }
219:
220: Object refObj = getProperty(VAR_TOOLBAR_COMP_REF);
221: if (refObj == null) {
222: return false;
223: }
224: return ((Reference) refObj).get() != null;
225: }
226:
227: /**
228: * This method is called if we are listening for changes on the set
229: * of open projecst and some project(s) is opened/closed.
230: */
231: public void stateChanged(ChangeEvent e) {
232: assert EventQueue.isDispatchThread();
233: if (shouldLog(LOG)) {
234: log("stateChanged()");
235: }
236:
237: /*
238: * Check whether listening on open projects is active.
239: */
240: if (getProperty(VAR_LISTENING) == null) {
241: return;
242: }
243:
244: if (checkToolbarPresenterExists()) {
245: updateState();
246: } else {
247: SearchScopeRegistry.getDefault().removeChangeListener(this );
248: putProperty(VAR_LISTENING, null);
249: putProperty(VAR_TOOLBAR_COMP_REF, null);
250: }
251: }
252:
253: @Override
254: public boolean isEnabled() {
255: assert EventQueue.isDispatchThread();
256: if (shouldLog(LOG)) {
257: log("isEnabled()");
258: }
259:
260: if (getProperty(VAR_LISTENING) != null) {
261: log(" - isListening");
262: return super .isEnabled();
263: } else if (getProperty(VAR_FIRST_ISENABLED) == null) {
264: log(" - checking registry");
265: return SearchScopeRegistry.getDefault()
266: .hasApplicableSearchScope();
267: } else {
268: /* first call of this method */
269: log(" - first \"isEnabled()\"");
270: putProperty(VAR_FIRST_ISENABLED, null);
271: return false;
272: }
273: }
274:
275: /**
276: */
277: private void updateState() {
278: assert EventQueue.isDispatchThread();
279: if (shouldLog(LOG)) {
280: log("updateState()");
281: }
282:
283: final boolean enabled = SearchScopeRegistry.getDefault()
284: .hasApplicableSearchScope();
285: Mutex.EVENT.writeAccess(new Runnable() {
286: public void run() {
287: setEnabled(enabled);
288: }
289: });
290: }
291:
292: @Override
293: protected String iconResource() {
294: return "org/openide/resources/actions/find.gif"; //NOI18N
295: }
296:
297: public String getName() {
298: String key = SearchScopeRegistry.getDefault()
299: .hasProjectSearchScopes() ? "LBL_Action_FindInProjects" //NOI18N
300: : "LBL_Action_FindInFiles"; //NOI18N
301: return NbBundle.getMessage(getClass(), key);
302: }
303:
304: public HelpCtx getHelpCtx() {
305: return new HelpCtx(FindInFilesAction.class);
306: }
307:
308: /** Perform this action. */
309: public void performAction() {
310: performAction(SearchScopeRegistry.getDefault()
311: .getSearchScopes(), null); //no preferred search scope
312: }
313:
314: private void performAction(Map<SearchScope, Boolean> searchScopes,
315: String preferredSearchScopeType) {
316: assert EventQueue.isDispatchThread();
317:
318: String msg = Manager.getInstance().mayStartSearching();
319: if (msg != null) {
320: /*
321: * Search cannot be started, for example because the replace
322: * operation has not finished yet.
323: */
324: DialogDisplayer.getDefault().notify(
325: new NotifyDescriptor.Message(msg,
326: NotifyDescriptor.INFORMATION_MESSAGE));
327: return;
328: }
329:
330: if (!isSomeEnabled(searchScopes)) {
331: return;
332: }
333:
334: boolean replacing = Boolean.TRUE.equals(getProperty(REPLACING));
335: SearchPanel searchPanel = new SearchPanel(searchScopes,
336: preferredSearchScopeType, replacing);
337:
338: searchPanel.showDialog();
339: if (searchPanel.getReturnStatus() != SearchPanel.RET_OK) {
340: return;
341: }
342:
343: SearchScope searchScope = searchPanel.getSearchScope();
344: BasicSearchCriteria basicSearchCriteria = searchPanel
345: .getBasicSearchCriteria();
346: List<SearchType> extraSearchTypes = searchPanel
347: .getSearchTypes();
348:
349: ResultView resultView = ResultView.getInstance();
350: resultView.rememberInput(searchScope.getTypeId(),
351: basicSearchCriteria, extraSearchTypes);
352: resultView.open();
353: resultView.requestActive();
354:
355: Manager.getInstance().scheduleSearchTask(
356: new SearchTask(searchScope, basicSearchCriteria,
357: searchPanel.getCustomizedSearchTypes()));
358: }
359:
360: /**
361: * Checks whether some of the {@code SearchScope}s is enabled.
362: *
363: * @param searchScopes search scopes and their states (enabled/disabled)
364: * @return {@code true} if at least some search scope is enabled,
365: * {@code false} otherwise
366: * @see SearchScopeRegistry#getSearchScopes
367: */
368: private static boolean isSomeEnabled(
369: Map<SearchScope, Boolean> searchScopes) {
370: for (Boolean b : searchScopes.values()) {
371: if (b) { //auto-unboxing
372: return true;
373: }
374: }
375: return false;
376: }
377:
378: @Override
379: protected boolean asynchronous() {
380: return false;
381: }
382:
383: private static final class LookupSensitive implements Action,
384: ChangeListener, Presenter.Menu, Presenter.Popup,
385: Presenter.Toolbar {
386:
387: private static int counter = 0;
388:
389: private final FindInFilesAction delegate;
390: private final SearchScopeRegistry searchScopeRegistry;
391: private final boolean searchSelection;
392: private final int id;
393:
394: /** support for listeners */
395: private PropertyChangeSupport support;
396: private boolean enabled;
397:
398: {
399: id = ++counter;
400: }
401:
402: LookupSensitive(FindInFilesAction delegate, Lookup lookup) {
403: this (delegate, lookup, false);
404: }
405:
406: LookupSensitive(FindInFilesAction delegate, Lookup lookup,
407: boolean searchSelection) {
408: this .delegate = delegate;
409: this .searchScopeRegistry = SearchScopeRegistry.getInstance(
410: lookup, id);
411: this .searchSelection = searchSelection;
412: log("<init>");
413: }
414:
415: private Object getLock() {
416: return this ;
417: }
418:
419: public Object getValue(String key) {
420: if (shouldLog(LOG)) {
421: log("getValue(\"" + key + "\")");
422: }
423: return delegate.getValue(key);
424: }
425:
426: public void putValue(String key, Object value) {
427: }
428:
429: public void actionPerformed(ActionEvent e) {
430: assert EventQueue.isDispatchThread();
431: if (shouldLog(LOG)) {
432: log("actionPerformed(...)");
433: }
434:
435: delegate.performAction(searchScopeRegistry
436: .getSearchScopes(),
437: searchSelection ? searchScopeRegistry
438: .getNodeSelectionSearchScope().getTypeId()
439: : null);
440: }
441:
442: public void setEnabled(boolean b) {
443: if (shouldLog(LOG)) {
444: log("setEnabled(" + b + ')');
445: }
446: }
447:
448: public boolean isEnabled() {
449: assert EventQueue.isDispatchThread();
450: if (shouldLog(LOG)) {
451: log("isEnabled(...)");
452: }
453:
454: synchronized (getLock()) {
455: if (support != null) {
456: return enabled;
457: }
458: }
459:
460: return searchScopeRegistry.hasApplicableSearchScope();
461: }
462:
463: public void addPropertyChangeListener(
464: PropertyChangeListener listener) {
465: if (shouldLog(LOG)) {
466: log("addPropertyChangeListener(...)");
467: }
468:
469: if (listener == null) {
470: return;
471: }
472:
473: synchronized (getLock()) {
474: if (support == null) {
475: support = new PropertyChangeSupport(this );
476: searchScopeRegistry.addChangeListener(this );
477: enabled = searchScopeRegistry
478: .hasApplicableSearchScope();
479: }
480: support.addPropertyChangeListener(listener);
481: }
482: }
483:
484: public void removePropertyChangeListener(
485: PropertyChangeListener listener) {
486: if (shouldLog(LOG)) {
487: log("removePropertyChangeListener(...)");
488: }
489:
490: if (listener == null) {
491: return;
492: }
493:
494: synchronized (getLock()) {
495: if (support == null) {
496: return;
497: }
498:
499: support.removePropertyChangeListener(listener);
500: boolean lastListener = !support.hasListeners(null);
501: if (lastListener) {
502: searchScopeRegistry.removeChangeListener(this );
503: support = null;
504: }
505: }
506: }
507:
508: public void stateChanged(ChangeEvent e) {
509: if (shouldLog(LOG)) {
510: log("stateChanged(...)");
511: }
512:
513: synchronized (getLock()) {
514: if (support != null) {
515:
516: boolean wasEnabled = enabled;
517: enabled = searchScopeRegistry
518: .hasApplicableSearchScope();
519:
520: /* notify the listeners: */
521: final PropertyChangeEvent newEvent = new PropertyChangeEvent(
522: this , PROP_ENABLED, wasEnabled, enabled);
523: final PropertyChangeListener[] listeners = support
524: .getPropertyChangeListeners();
525: Mutex.EVENT.writeAccess(new Runnable() {
526: public void run() {
527: for (PropertyChangeListener l : listeners) {
528: l.propertyChange(newEvent);
529: }
530: }
531: });
532: }
533: }
534: }
535:
536: public JMenuItem getMenuPresenter() {
537: if (shouldLog(LOG)) {
538: log("getMenuPresenter(...)");
539: }
540: return delegate.getMenuPresenter();
541: }
542:
543: public JMenuItem getPopupPresenter() {
544: if (shouldLog(LOG)) {
545: log("getPopupPresenter(...)");
546: }
547: return delegate.getPopupPresenter();
548: }
549:
550: public Component getToolbarPresenter() {
551: if (shouldLog(LOG)) {
552: log("getToolbarPresenter(...)");
553: }
554: return delegate.getToolbarPresenter();
555: }
556:
557: @Override
558: public String toString() {
559: return shortClassName + " #" + id;
560: }
561:
562: private final String shortClassName;
563:
564: {
565: String clsName = getClass().getName();
566: int lastDot = clsName.lastIndexOf('.');
567: shortClassName = ((lastDot != -1) ? clsName
568: .substring(lastDot + 1) : clsName)
569: .replace('$', '.');
570: }
571:
572: private boolean shouldLog(Logger logger) {
573: return logger.isLoggable(FINER)
574: && shortClassName.startsWith("FindInFilesAction");
575: }
576:
577: private void log(String msg) {
578: LOG.finer(this + ": " + msg);
579: }
580:
581: }
582:
583: private final String shortClassName;
584:
585: {
586: String clsName = getClass().getName();
587: int lastDot = clsName.lastIndexOf('.');
588: shortClassName = (lastDot != -1) ? clsName
589: .substring(lastDot + 1) : clsName;
590: }
591:
592: private boolean shouldLog(Logger logger) {
593: return logger.isLoggable(FINER)
594: && shortClassName.equals("FindInFilesAction");
595: }
596:
597: private void log(String msg) {
598: LOG.finer(shortClassName + ": " + msg);
599: }
600:
601: }
|