Today I got some help from Jaroslav Havlin, the creator of the new "Search in Projects API".
Below are the steps to create a search provider that finds recently modified files, via a new tab in the "Find in Projects" dialog:
Here's how to get to the above result.
Create a new NetBeans module project named "RecentlyModifiedFilesSearch". Then set dependencies on these libraries:
Search in Projects API
Lookup API
Utilities API
Dialogs API
Datasystems API
File System API
Nodes API
Create and register an implementation of "SearchProvider". This class tells the application the name of the provider and how it can be used. It should be registered via the @ServiceProvider annotation.Methods to implement:
Method createPresenter creates a new object that is added to the "Find in Projects" dialog when it is opened.
Method isReplaceSupported should return true if this provider support replacing, not only searching.
If you want to disable the search provider (e.g., there aren't required external tools available in the OS), return false from isEnabled.
Method getTitle returns a string that will be shown in the tab in the "Find in Projects" dialog. It can be localizable.
Example file "org.netbeans.example.search.ExampleSearchProvider":
package org.netbeans.example.search;
import org.netbeans.spi.search.provider.SearchProvider;
import org.netbeans.spi.search.provider.SearchProvider.Presenter;
import org.openide.util.lookup.ServiceProvider;
@ServiceProvider(service = SearchProvider.class)
public class ExampleSearchProvider extends SearchProvider {
@Override
public Presenter createPresenter(boolean replaceMode) {
return new ExampleSearchPresenter(this);
}
@Override
public boolean isReplaceSupported() {
return false;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getTitle() {
return "Recent Files Search";
}
}
Next, we need to create a SearchProvider.Presenter. This is an object that is passed to the "Find in Projects" dialog and contains a visual component to show in the dialog, together with some methods to interact with it.Methods to implement:
Method getForm returns a JComponent that should contain controls for various search criteria. In the example below, we have controls for a file name pattern, search scope, and the age of files.
Method isUsable is called by the dialog to check whether the Find button should be enabled or not. You can use NotificationLineSupport passed as its argument to set a display error, warning, or info message.
Method composeSearch is used to apply the settings and prepare a search task. It returns a SearchComposition object, as shown below.
Please note that the example uses ComponentUtils.adjustComboForFileName (and similar methods), that modifies a JComboBox component to act as a combo box for selection of file name pattern. These methods were designed to make working with components created in a GUI Builder comfortable.
Remember to call fireChange whenever the value of any criteria changes.
Example file "org.netbeans.example.search.ExampleSearchPresenter":
package org.netbeans.example.search;
import java.awt.FlowLayout;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.search.SearchScopeOptions;
import org.netbeans.api.search.ui.ComponentUtils;
import org.netbeans.api.search.ui.FileNameController;
import org.netbeans.api.search.ui.ScopeController;
import org.netbeans.api.search.ui.ScopeOptionsController;
import org.netbeans.spi.search.provider.SearchComposition;
import org.netbeans.spi.search.provider.SearchProvider;
import org.openide.NotificationLineSupport;
import org.openide.util.HelpCtx;
public class ExampleSearchPresenter extends SearchProvider.Presenter {
private JPanel panel = null;
ScopeOptionsController scopeSettingsPanel;
FileNameController fileNameComboBox;
ScopeController scopeComboBox;
ChangeListener changeListener;
JSlider slider;
public ExampleSearchPresenter(SearchProvider searchProvider) {
super(searchProvider, false);
}
/**
* Get UI component that can be added to the search dialog.
*/
@Override
public synchronized JComponent getForm() {
if (panel == null) {
panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEADING));
JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEADING));
JPanel row3 = new JPanel(new FlowLayout(FlowLayout.LEADING));
row1.add(new JLabel("Age in hours: "));
slider = new JSlider(1, 72);
row1.add(slider);
final JLabel hoursLabel = new JLabel(String.valueOf(slider.getValue()));
row1.add(hoursLabel);
row2.add(new JLabel("File name: "));
fileNameComboBox = ComponentUtils.adjustComboForFileName(new JComboBox());
row2.add(fileNameComboBox.getComponent());
scopeSettingsPanel = ComponentUtils.adjustPanelForOptions(new JPanel(),
false, fileNameComboBox);
row3.add(new JLabel("Scope: "));
scopeComboBox = ComponentUtils.adjustComboForScope(new JComboBox(), null);
row3.add(scopeComboBox.getComponent());
panel.add(row1);
panel.add(row3);
panel.add(row2);
panel.add(scopeSettingsPanel.getComponent());
initChangeListener();
slider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
hoursLabel.setText(String.valueOf(slider.getValue()));
}
});
}
return panel;
}
private void initChangeListener() {
this.changeListener = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
fireChange();
}
};
fileNameComboBox.addChangeListener(changeListener);
scopeSettingsPanel.addChangeListener(changeListener);
slider.addChangeListener(changeListener);
}
@Override
public HelpCtx getHelpCtx() {
return null; // Some help should be provided, omitted for simplicity.
}
/**
* Create search composition for criteria specified in the form.
*/
@Override
public SearchComposition<?> composeSearch() {
SearchScopeOptions sso = scopeSettingsPanel.getSearchScopeOptions();
return new ExampleSearchComposition(sso, scopeComboBox.getSearchInfo(),
slider.getValue(), this);
}
/**
* Here we return always true, but could return false e.g. if file name
* pattern is empty.
*/
@Override
public boolean isUsable(NotificationLineSupport notifySupport) {
return true;
}
}
The last part of our search provider is the implementation of SearchComposition. This is a composition of various search parameters, the actual search algorithm, and the displayer that presents the results.Methods to implement:
The most important method here is start, which performs the actual search. In this case, SearchInfo and SearchScopeOptions objects are used for traversing. These objects were provided by controllers of GUI components (in the presenter). When something interesting is found, it should be displayed (with SearchResultsDisplayer.addMatchingObject).
Method getSearchResultsDisplayer should return the displayer associated with this composition. The displayer can be created by subclassing SearchResultsDisplayer class or simply by using the SearchResultsDisplayer.createDefault. Then you only need a helper object that can create nodes for found objects.
Example file "org.netbeans.example.search.ExampleSearchComposition":
package org.netbeans.example.search;
public class ExampleSearchComposition extends SearchComposition<DataObject> {
SearchScopeOptions searchScopeOptions;
SearchInfo searchInfo;
int oldInHours;
SearchResultsDisplayer<DataObject> resultsDisplayer;
private final Presenter presenter;
AtomicBoolean terminated = new AtomicBoolean(false);
public ExampleSearchComposition(SearchScopeOptions searchScopeOptions,
SearchInfo searchInfo, int oldInHours, Presenter presenter) {
this.searchScopeOptions = searchScopeOptions;
this.searchInfo = searchInfo;
this.oldInHours = oldInHours;
this.presenter = presenter;
}
@Override
public void start(SearchListener listener) {
for (FileObject fo : searchInfo.getFilesToSearch(
searchScopeOptions, listener, terminated)) {
if (ageInHours(fo) < oldInHours) {
try {
DataObject dob = DataObject.find(fo);
getSearchResultsDisplayer().addMatchingObject(dob);
} catch (DataObjectNotFoundException ex) {
listener.fileContentMatchingError(fo.getPath(), ex);
}
}
}
}
@Override
public void terminate() {
terminated.set(true);
}
@Override
public boolean isTerminated() {
return terminated.get();
}
/**
* Use default displayer to show search results.
*/
@Override
public synchronized SearchResultsDisplayer<DataObject> getSearchResultsDisplayer() {
if (resultsDisplayer == null) {
resultsDisplayer = createResultsDisplayer();
}
return resultsDisplayer;
}
private SearchResultsDisplayer<DataObject> createResultsDisplayer() {
/**
* Object to transform matching objects to nodes.
*/
SearchResultsDisplayer.NodeDisplayer<DataObject> nd =
new SearchResultsDisplayer.NodeDisplayer<DataObject>() {
@Override
public org.openide.nodes.Node matchToNode(
final DataObject match) {
return new FilterNode(match.getNodeDelegate()) {
@Override
public String getDisplayName() {
return super.getDisplayName()
+ " (" + ageInMinutes(match.getPrimaryFile()) + " minutes old)";
}
};
}
};
return SearchResultsDisplayer.createDefault(nd, this,
presenter, "less than " + oldInHours + " hours old");
}
private static long ageInMinutes(FileObject fo) {
long fileDate = fo.lastModified().getTime();
long now = System.currentTimeMillis();
return (now - fileDate) / 60000;
}
private static long ageInHours(FileObject fo) {
return ageInMinutes(fo) / 60;
}
}
Run the module, select a node in the Projects window, press Ctrl-F, and you'll see the "Find in Projects" dialog has two tabs, the second is the one you provided above: