/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* SearchDialog.java
* Creation date: (Aug 31, 2005)
* By: Jawright
*/
package org.openquark.gems.client;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListModel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.openquark.cal.compiler.CompilerMessage;
import org.openquark.cal.compiler.CompilerMessageLogger;
import org.openquark.cal.compiler.MessageLogger;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.Name;
import org.openquark.cal.compiler.SearchResult;
import org.openquark.cal.compiler.SourceMetrics;
import org.openquark.cal.compiler.SourcePosition;
import org.openquark.cal.compiler.SourceRange;
import org.openquark.cal.services.CALFeatureName;
import org.openquark.cal.services.CALSourceManager;
import org.openquark.cal.services.NullaryEnvironment;
import org.openquark.cal.services.Perspective;
import org.openquark.cal.services.ResourceName;
import org.openquark.cal.services.ResourceNullaryStore;
import org.openquark.cal.services.ResourcePathStore;
import org.openquark.cal.services.ResourceStore;
import org.openquark.cal.services.Status;
/**
* Dialog for prompting the user for search
*
* @author Jawright
*/
class SearchDialog extends JDialog {
private static final long serialVersionUID = -7152564719622388083L;
/**
* Wrapper class for SearchResults that will be placed into a UI element.
* The toString() method returns a localized string suitable for display
* to users.
*
* @author Jawright
*/
private static abstract class SearchResultItem {
/**
* Wrapper class for precise SearchResults that will be placed into a UI element.
* The toString() method returns a localized string suitable for display
* to users.
*
* @author James Wright
*/
private static final class Precise extends SearchResultItem {
private final SearchResult.Precise searchResult;
private Precise(SearchResult.Precise searchResult) {
this.searchResult = searchResult;
}
@Override
public String toString() {
SourcePosition sourcePosition = searchResult.getSourcePosition();
return GemCutterMessages.getString("SD_SearchResultTemplateLong", new Object[] {
sourcePosition.getSourceName(),
Integer.valueOf(sourcePosition.getLine()),
Integer.valueOf(sourcePosition.getColumn()),
searchResult.getName().toSourceText()
});
}
SourcePosition getSourcePosition() {
return searchResult.getSourcePosition();
}
SourceRange getSourceRange() {
return searchResult.getSourceRange();
}
Name getQualifiedName() {
return searchResult.getName();
}
}
/**
* Wrapper class for search hits based only on frequency information that will be placed into a UI element.
* The toString() method returns a localized string suitable for display
* to users.
*
* @author Joseph Wong
*/
private static final class Frequency extends SearchResultItem {
private final SearchResult.Frequency searchResult;
private Frequency(SearchResult.Frequency searchResult) {
this.searchResult = searchResult;
}
@Override
public String toString() {
SearchResult.Frequency.Type type = searchResult.getType();
String template = null;
if (type == SearchResult.Frequency.Type.FUNCTION_REFERENCES) {
template = "SD_SearchResultFrequencyTypeFunctionReferences";
} else if (type == SearchResult.Frequency.Type.INSTANCE_METHOD_REFERENCES) {
template = "SD_SearchResultFrequencyTypeInstanceMethodReferences";
} else if (type == SearchResult.Frequency.Type.CLASS_CONSTRAINTS) {
template = "SD_SearchResultFrequencyTypeClassConstraints";
} else if (type == SearchResult.Frequency.Type.CLASSES) {
template = "SD_SearchResultFrequencyTypeClasses";
} else if (type == SearchResult.Frequency.Type.INSTANCES) {
template = "SD_SearchResultFrequencyTypeInstances";
} else if (type == SearchResult.Frequency.Type.DEFINITION) {
template = "SD_SearchResultFrequencyTypeDefinition";
} else if (type == SearchResult.Frequency.Type.IMPORT) {
template = "SD_SearchResultFrequencyTypeImport";
} else {
throw new IllegalStateException("Unknown search result type: " + type);
}
return GemCutterMessages.getString(template, new Object[] {
searchResult.getModuleName(),
Integer.valueOf(searchResult.getFrequency()),
searchResult.getName().toSourceText()
});
}
Name getName() {
return searchResult.getName();
}
}
}
/** Used to perform the searches */
private final SourceMetrics workspaceSourceMetrics;
/** Root of preference keys of the form previousSearches0, previousSearches1, etc.
* For storing past searches for the combobox. */
private static final String PREVIOUS_SEARCHES_PREF_KEY_ROOT = "previousSearches";
/** Preference key for the type of search radio button */
private static final String SEARCH_TYPE_PREF_KEY = "searchType";
/** Preference key for x position of search dialog */
private static final String SEARCH_DIALOG_X_PREF_KEY = "searchDialogX";
/** Preference key for y position of search dialog */
private static final String SEARCH_DIALOG_Y_PREF_KEY = "searchDialogY";
/** Preference key for width of search dialog */
private static final String SEARCH_DIALOG_WIDTH_PREF_KEY = "searchDialogWidth";
/** Preference key for height of search dialog */
private static final String SEARCH_DIALOG_HEIGHT_PREF_KEY = "searchDialogHeight";
/** An empty array used for clearing the search results pane. */
private static final Object[] EMPTY_ARRAY = new Object[0];
/** Number of previous search targets to save */
private static final int numSavedSearches = 10;
/** UI element (text field w/drop-down) for the user to enter the search text into */
private final JComboBox searchText = new JComboBox();
/** Manages radio button mutual-exclusivity */
private final ButtonGroup radioGroup = new ButtonGroup();
/** when selected we will search for all occurrences of an entity */
private final JRadioButton allOccurrencesButton = new JRadioButton(GemCutter.getResourceString("SD_AllOccurrences"));
/** when selected we will search for references to a gem */
private final JRadioButton referencesButton = new JRadioButton(GemCutter.getResourceString("SD_References"));
/** when selected we will search for definitions of a gem or type */
private final JRadioButton definitionButton = new JRadioButton(GemCutter.getResourceString("SD_Definition"));
/** when selected we will search for instances for a class */
private final JRadioButton instancesButton = new JRadioButton(GemCutter.getResourceString("SD_Instances"));
/** when selected we will search for instances for a type */
private final JRadioButton classesButton = new JRadioButton(GemCutter.getResourceString("SD_Classes"));
/** when selected we will search for instances for a type */
private final JRadioButton constructionsButton = new JRadioButton(GemCutter.getResourceString("SD_Constructions"));
/** List of SourcePositions */
private final JList searchResultsPane = new JList();
/** Scroll support for the searchResultsPane */
private final JScrollPane searchResultsScrollPane = new JScrollPane(searchResultsPane);
/** Area of the dialog for outputting status information to the user */
private final JLabel statusLabel = new JLabel(" ");
/** OK button for the dialog */
private final JButton okButton = new JButton(GemCutter.getResourceString("SD_OK"));
/** Cancel button for the dialog */
private final JButton cancelButton = new JButton(GemCutter.getResourceString("SD_Cancel"));
/** Modeless result display window */
private final SearchResultsDialog searchResultsDialog;
/** The most recently searched-for target. This prevents needless repeated searches. */
private String latestSearchTarget = null;
/** The most recently type of search. This prevents needless repeated searches. */
private SearchType latestSearchType = null;
/**
* The thread object for the currently-running search, if any. This field should
* only be read or modified from the AWT thread.
*/
private Thread pendingSearchThread = null;
/**
* The parent window for this dialog
*/
private final JFrame parent;
/**
* @param parent Parent window
*/
SearchDialog(JFrame parent, Perspective perspective) {
super(parent);
this.parent = parent;
this.workspaceSourceMetrics = perspective.getWorkspace().getSourceMetrics();
searchResultsDialog = new SearchResultsDialog(parent, perspective);
initialize();
}
/** Set up the UI */
private void initialize() {
// Basic window properties
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setTitle(GemCutter.getResourceString("SearchDialog"));
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
setContentPane(mainPanel);
// Widget setup
searchText.setEditable(true);
searchText.setMinimumSize(new Dimension(235, 23));
searchText.setMaximumSize(new Dimension(Integer.MAX_VALUE, 23));
// Layout
Box radioBox = Box.createHorizontalBox();
Box radioInternalBox = Box.createVerticalBox();
radioInternalBox.add(allOccurrencesButton);
radioInternalBox.add(referencesButton);
radioInternalBox.add(definitionButton);
radioInternalBox.add(instancesButton);
radioInternalBox.add(classesButton);
radioInternalBox.add(constructionsButton);
radioGroup.add(allOccurrencesButton);
radioGroup.add(referencesButton);
radioGroup.add(definitionButton);
radioGroup.add(instancesButton);
radioGroup.add(classesButton);
radioGroup.add(constructionsButton);
radioBox.add(radioInternalBox);
radioBox.add(Box.createHorizontalGlue());
Box statusBox = Box.createHorizontalBox();
statusBox.add(statusLabel);
statusBox.add(Box.createHorizontalGlue());
Box centerBox = Box.createVerticalBox();
centerBox.add(radioBox);
centerBox.add(statusBox);
centerBox.add(searchResultsScrollPane);
centerBox.add(Box.createVerticalStrut(10));
Box buttonBox = Box.createHorizontalBox();
buttonBox.add(Box.createHorizontalGlue());
buttonBox.add(okButton);
buttonBox.add(Box.createHorizontalStrut(10));
buttonBox.add(cancelButton);
getContentPane().add(searchText, "North");
getContentPane().add(centerBox, "Center");
getContentPane().add(buttonBox, "South");
// Actions
okButton.setAction(
new AbstractAction(GemCutter.getResourceString("SD_OK")) {
private static final long serialVersionUID = -2680555515628543174L;
public void actionPerformed(ActionEvent e) {
performSearch();
}
});
cancelButton.setAction(
new AbstractAction(GemCutter.getResourceString("SD_Cancel")) {
private static final long serialVersionUID = 508073377921589363L;
public void actionPerformed(ActionEvent e) {
SearchDialog.this.dispose();
}
});
searchResultsPane.addListSelectionListener(new ListSelectionListener () {
public void valueChanged(ListSelectionEvent evt) {
SearchResultItem searchResultItem = (SearchResultItem)searchResultsPane.getSelectedValue();
if (searchResultItem instanceof SearchResultItem.Precise) {
searchResultsDialog.setResult((SearchResultItem.Precise)searchResultItem);
searchResultsDialog.setVisible(true);
}
}
});
searchResultsPane.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int index = searchResultsPane.locationToIndex(e.getPoint());
SearchResultItem searchResultItem = (SearchResultItem)searchResultsPane.getModel().getElementAt(index);
if (searchResultItem instanceof SearchResultItem.Precise) {
searchResultsDialog.setResult((SearchResultItem.Precise)searchResultItem);
searchResultsDialog.setVisible(true);
}
}
}
});
searchText.getEditor().getEditorComponent().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent evt) {
// Enter typed in the text entry field starts a search
if(evt.getKeyCode() == KeyEvent.VK_ENTER) {
performSearch();
// Escape exits the dialog
} else if(evt.getKeyCode() == KeyEvent.VK_ESCAPE) {
SearchDialog.this.dispose();
}
}
});
// Enable or disable the OK button depending upon whether a valid target has been entered
// Enable or disable the class/type-only radio buttons depending upon whether or not an
// upper-case identifier has been entered.
Component editor = searchText.getEditor().getEditorComponent();
if(editor instanceof JTextField) {
JTextField searchTextField = (JTextField)editor;
searchTextField.getDocument().addDocumentListener(
new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
updateButtonState();
}
public void removeUpdate(DocumentEvent e) {
updateButtonState();
}
public void changedUpdate(DocumentEvent e) {
updateButtonState();
}
});
}
// If the user changes which radio button is selected, we may need to enable the
// OK button again.
ChangeListener buttonChangeHandler =
new ChangeListener() {
public void stateChanged(ChangeEvent evt) {
updateButtonState();
}
};
referencesButton.addChangeListener(buttonChangeHandler);
definitionButton.addChangeListener(buttonChangeHandler);
instancesButton.addChangeListener(buttonChangeHandler);
classesButton.addChangeListener(buttonChangeHandler);
constructionsButton.addChangeListener(buttonChangeHandler);
// Restore preferences
for(int i = 0; i < numSavedSearches; i++) {
String previousSearch = GemCutter.getPreferences().get(PREVIOUS_SEARCHES_PREF_KEY_ROOT + i, null);
if(previousSearch != null) {
searchText.addItem(previousSearch);
} else {
break;
}
}
searchText.setSelectedIndex(-1); // Start with a blank text area
SearchType searchType;
try {
String searchTypeStr = GemCutter.getPreferences().get(SEARCH_TYPE_PREF_KEY, SearchType.REFERENCES.toString());
searchType = SearchType.fromString(searchTypeStr);
} catch(IllegalArgumentException e) {
searchType = SearchType.ALL;
}
if(searchType == SearchType.ALL) {
allOccurrencesButton.setSelected(true);
} else if(searchType == SearchType.REFERENCES) {
referencesButton.setSelected(true);
} else if(searchType == SearchType.DEFINITION) {
definitionButton.setSelected(true);
} else if(searchType == SearchType.INSTANCES) {
instancesButton.setSelected(true);
} else if(searchType == SearchType.CLASSES) {
classesButton.setSelected(true);
} else if(searchType == SearchType.CONSTRUCTIONS) {
constructionsButton.setSelected(true);
} else {
throw new IllegalStateException("Unknown searchType");
}
// Commit
setModal(false);
pack();
setSize(new Dimension(365, 400));
okButton.setEnabled(false);
}
/** Update the enabled/disabled state of the ok button and radio buttons */
private void updateButtonState() {
String candidateString = getSearchString();
if(candidateString == null || candidateString.length() == 0) {
okButton.setEnabled(false);
instancesButton.setEnabled(true);
classesButton.setEnabled(true);
constructionsButton.setEnabled(true);
} else {
okButton.setEnabled(true);
instancesButton.setEnabled(true);
classesButton.setEnabled(true);
constructionsButton.setEnabled(true);
}
}
/** Save the UI state to be restored next time we show the dialog */
private void savePreferences() {
for(int i = 0; i < numSavedSearches && i < searchText.getItemCount(); i++) {
GemCutter.getPreferences().put(PREVIOUS_SEARCHES_PREF_KEY_ROOT + i, searchText.getItemAt(i).toString());
}
// Save the current search type
if(latestSearchType != null) {
GemCutter.getPreferences().put(SEARCH_TYPE_PREF_KEY, latestSearchType.toString());
}
}
/** Update the status area with number of hits found */
private void displayHitCount() {
int nHits = 0;
ListModel searchResultItems = searchResultsPane.getModel();
for (int i = 0, n = searchResultItems.getSize(); i < n; i++) {
SearchResultItem item = (SearchResultItem)searchResultItems.getElementAt(i);
if (item instanceof SearchResultItem.Frequency) {
nHits += ((SearchResultItem.Frequency)item).searchResult.getFrequency();
} else {
nHits++;
}
}
if(getSearchType() == SearchType.ALL) {
if(nHits == 1) {
statusLabel.setText(GemCutterMessages.getString("SD_OneOccurrenceCount", getSearchString()));
} else {
statusLabel.setText(GemCutterMessages.getString("SD_OccurrenceCount", Integer.valueOf(nHits), getSearchString()));
}
} else if(getSearchType() == SearchType.REFERENCES) {
if(nHits == 1) {
statusLabel.setText(GemCutterMessages.getString("SD_OneReferenceCount", getSearchString()));
} else {
statusLabel.setText(GemCutterMessages.getString("SD_ReferenceCount", Integer.valueOf(nHits), getSearchString()));
}
} else if(getSearchType() == SearchType.DEFINITION) {
if(nHits == 1) {
statusLabel.setText(GemCutterMessages.getString("SD_OneDefinitionCount", getSearchString()));
} else {
statusLabel.setText(GemCutterMessages.getString("SD_DefinitionCount", Integer.valueOf(nHits), getSearchString()));
}
} else if(getSearchType() == SearchType.INSTANCES) {
if(nHits == 1) {
statusLabel.setText(GemCutterMessages.getString("SD_OneInstanceCount", getSearchString()));
} else {
statusLabel.setText(GemCutterMessages.getString("SD_InstanceCount", Integer.valueOf(nHits), getSearchString()));
}
} else if(getSearchType() == SearchType.CLASSES) {
if(nHits == 1) {
statusLabel.setText(GemCutterMessages.getString("SD_OneClassCount", getSearchString()));
} else {
statusLabel.setText(GemCutterMessages.getString("SD_ClassCount", Integer.valueOf(nHits), getSearchString()));
}
} else if(getSearchType() == SearchType.CONSTRUCTIONS) {
if(nHits == 1) {
statusLabel.setText(GemCutterMessages.getString("SD_OneConstructionCount", getSearchString()));
} else {
statusLabel.setText(GemCutterMessages.getString("SD_ConstructionCount", Integer.valueOf(nHits), getSearchString()));
}
} else {
// This should never happen, since getSearchType will return SearchType.ALL if it
// doesn't understand the current UI selection.
throw new IllegalStateException("invalid current search type");
}
}
/**
* Perform a new search based on the current UI state.
* This method should only be called from the AWT thread.
*/
private void performSearch() {
if(isSearchPending()) {
return;
}
final String searchString = getSearchString();
final SearchType searchType = getSearchType();
if(searchString == null ||
searchString.equals(latestSearchTarget) && searchType == latestSearchType) {
return;
}
latestSearchTarget = searchString;
latestSearchType = searchType;
// We want the latest search always to appear at the top of the list, but we
// also want each search to appear in the list only once, so remove any duplicates
// before re-inserting the new search at the top of the list.
for(int i = 0; i < searchText.getItemCount(); i++) {
String itemString = searchText.getItemAt(i).toString();
if(itemString.equals(searchString)) {
searchText.removeItemAt(i);
i--;
}
}
searchText.insertItemAt(searchString, 0);
searchText.setSelectedIndex(0);
savePreferences();
okButton.setEnabled(false);
searchText.setEnabled(false);
statusLabel.setText(GemCutter.getResourceString("SD_Searching"));
searchResultsPane.setListData(EMPTY_ARRAY);
final Cursor oldCursor = getCursor();
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
// Run the search on its own thread, and then call into the AWT thread
// to update the UI.
pendingSearchThread = new Thread("search thread") {
@Override
public void run() {
List<SearchResult> newResults = Collections.<SearchResult>emptyList();
final MessageLogger messageLogger = new MessageLogger();
try {
if(searchType == SearchType.ALL) {
newResults = workspaceSourceMetrics.findAllOccurrences(searchString, messageLogger);
} else if(searchType == SearchType.REFERENCES) {
newResults = workspaceSourceMetrics.findReferences(searchString, messageLogger);
} else if(searchType == SearchType.DEFINITION) {
newResults = workspaceSourceMetrics.findDefinition(searchString, messageLogger);
} else if(searchType == SearchType.INSTANCES) {
newResults = workspaceSourceMetrics.findInstancesOfClass(searchString, messageLogger);
} else if(searchType == SearchType.CLASSES) {
newResults = workspaceSourceMetrics.findTypeInstances(searchString, messageLogger);
} else if(searchType == SearchType.CONSTRUCTIONS) {
newResults = workspaceSourceMetrics.findConstructions(searchString, messageLogger);
} else {
throw new IllegalStateException("unknown SearchType");
}
} finally {
final Vector<SearchResultItem> newItems = new Vector<SearchResultItem>(newResults.size());
for(int i = 0; i < newResults.size(); i++) {
SearchResult searchResult = newResults.get(i);
SearchResultItem searchResultItem;
if (searchResult instanceof SearchResult.Precise) {
searchResultItem = new SearchResultItem.Precise((SearchResult.Precise)searchResult);
} else if (searchResult instanceof SearchResult.Frequency){
searchResultItem = new SearchResultItem.Frequency((SearchResult.Frequency)searchResult);
} else {
throw new IllegalStateException("unknown SearchResult");
}
newItems.add(searchResultItem);
}
// We use invokeLater to ensure that the searchComplete method will be
// called on the AWT thread.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
searchComplete(newItems, messageLogger, oldCursor);
}
});
}
}
};
pendingSearchThread.start();
}
/**
* Perform end-of-search processing (including clearing the current-search thread).
* This method should only be called from the AWT thread.
* @param results Vector of SourcePositionItems representing search hits
* @param messageLogger Logger used during search
* @param savedCursor Cursor to restore
*/
private void searchComplete(Vector<SearchResultItem> results, CompilerMessageLogger messageLogger, Cursor savedCursor) {
searchResultsPane.setListData(results);
okButton.setEnabled(true);
searchText.setEnabled(true);
displayHitCount();
setCursor(savedCursor);
pendingSearchThread = null;
if(messageLogger.getMaxSeverity().compareTo(CompilerMessage.Severity.ERROR) >= 0) {
StringBuilder msgText = new StringBuilder(GemCutter.getResourceString("SD_ParseErrorsDuringSearch") + "\n" + GemCutter.getResourceString("CompileErrorIntro"));
for (final CompilerMessage message : messageLogger.getCompilerMessages()) {
msgText.append("\n ");
msgText.append(message.toString());
}
JOptionPane.showMessageDialog(parent, msgText, GemCutter.getResourceString("WindowTitle"), JOptionPane.WARNING_MESSAGE);
}
// As a convenience, we pop up the results window directly if there is
// only a single result
if(results.size() == 1) {
SearchResultItem searchResultItem = results.get(0);
if (searchResultItem instanceof SearchResultItem.Precise) {
searchResultsDialog.setResult((SearchResultItem.Precise)searchResultItem);
searchResultsDialog.setVisible(true);
}
}
}
/** @return SearchType the type of search the user has requested */
SearchType getSearchType() {
if(allOccurrencesButton.isSelected()) {
return SearchType.ALL;
} else if(referencesButton.isSelected()) {
return SearchType.REFERENCES;
} else if(definitionButton.isSelected()) {
return SearchType.DEFINITION;
} else if(instancesButton.isSelected()) {
return SearchType.INSTANCES;
} else if(classesButton.isSelected()) {
return SearchType.CLASSES;
} else if(constructionsButton.isSelected()) {
return SearchType.CONSTRUCTIONS;
} else {
return SearchType.ALL;
}
}
/** Set the current type of search */
private void setSearchType(SearchType searchType) {
if(searchType == SearchType.ALL) {
allOccurrencesButton.setSelected(true);
} else if(searchType == SearchType.REFERENCES) {
referencesButton.setSelected(true);
} else if(searchType == SearchType.DEFINITION) {
definitionButton.setSelected(true);
} else if(searchType == SearchType.INSTANCES) {
instancesButton.setSelected(true);
} else if(searchType == SearchType.CLASSES) {
classesButton.setSelected(true);
} else if(searchType == SearchType.CONSTRUCTIONS) {
constructionsButton.setSelected(true);
} else {
allOccurrencesButton.setSelected(true);
}
}
/** @return The current text of the search target field
*/
String getSearchString() {
return (String)searchText.getEditor().getItem();
}
/**
* Sets the text of the search target
* @param newText String to set the search target to
*/
private void setSearchText(String newText) {
searchText.getEditor().setItem(newText);
}
/**
* Perform a search of the specified type for the specified text
* @param searchText String text to search for
* @param searchType SearchType enum value of type of search
*/
void performSearch(String searchText, SearchType searchType) {
if(isSearchPending()) {
return;
}
setSearchText(searchText);
setSearchType(searchType);
performSearch();
}
/**
* This method should only be called from the AWT thread.
* @return true if a search is already pending, or false otherwise.
*/
boolean isSearchPending() {
return (pendingSearchThread != null);
}
/**
* Save the dialog's position and size into the GemCutter's preferences.
*/
void savePosition() {
GemCutter.getPreferences().putInt(SEARCH_DIALOG_X_PREF_KEY, getX());
GemCutter.getPreferences().putInt(SEARCH_DIALOG_Y_PREF_KEY, getY());
GemCutter.getPreferences().putInt(SEARCH_DIALOG_WIDTH_PREF_KEY, getWidth());
GemCutter.getPreferences().putInt(SEARCH_DIALOG_HEIGHT_PREF_KEY, getHeight());
}
/**
* @return true if the preferences contain position and size info for this dialog
*/
boolean hasSavedPosition() {
return (GemCutter.getPreferences().getInt(SEARCH_DIALOG_X_PREF_KEY, -1) != -1 &&
GemCutter.getPreferences().getInt(SEARCH_DIALOG_Y_PREF_KEY, -1) != -1 &&
GemCutter.getPreferences().getInt(SEARCH_DIALOG_WIDTH_PREF_KEY, -1) != -1 &&
GemCutter.getPreferences().getInt(SEARCH_DIALOG_HEIGHT_PREF_KEY, -1) != -1);
}
/**
* Set the bounds of the dialog to the position and size stored in the preferences
*/
void setPositionToSaved() {
setBounds(GemCutter.getPreferences().getInt(SEARCH_DIALOG_X_PREF_KEY, 0),
GemCutter.getPreferences().getInt(SEARCH_DIALOG_Y_PREF_KEY, 0),
GemCutter.getPreferences().getInt(SEARCH_DIALOG_WIDTH_PREF_KEY, 600),
GemCutter.getPreferences().getInt(SEARCH_DIALOG_HEIGHT_PREF_KEY, 800));
}
/**
* Provides a modeless dialogue for displaying the source of a module with a
* hit highlighted.
*
* @author Jawright
*/
private static final class SearchResultsDialog extends JDialog {
private static final long serialVersionUID = -122928860414946366L;
/** Preference key for x position of search results dialog */
private static final String SEARCH_RESULTS_DIALOG_X_PREF_KEY = "searchResultsDialogX";
/** Preference key for y position of search results dialog */
private static final String SEARCH_RESULTS_DIALOG_Y_PREF_KEY = "searchResultsDialogY";
/** Preference key for width of search results dialog */
private static final String SEARCH_RESULTS_DIALOG_WIDTH_PREF_KEY = "searchResultsDialogWidth";
/** Preference key for height of search results dialog */
private static final String SEARCH_RESULTS_DIALOG_HEIGHT_PREF_KEY = "searchResultsDialogHeight";
/** Used for finding and loading/saving modules based on their names */
private final Perspective perspective;
/** When clicked, we try to save the file */
private final JButton saveButton = new JButton(GemCutter.getResourceString("SD_Save"));
/** When clicked, we hide the dialog */
private final JButton closeButton = new JButton(GemCutter.getResourceString("SD_Close"));
/** Text area where the source is displayed and (potentially) edited */
private final JTextArea editorPane = new JTextArea();
/** Provides scroll functionality for the editorPane */
private final JScrollPane scrollPane = new JScrollPane(editorPane);
/** SourcePosition of the current search hit */
private SourcePosition currentSourcePosition = null;
/** When true, it is okay for the user to edit this file */
private boolean isEditable = false;
/** Size of the parent frame */
private final Rectangle parentBounds;
/**
* Construct a new SearchResultsDialog.
* @param parent Parent window for this dialog
* @param perspective Perspective to obtain Workspace from for searching
*/
SearchResultsDialog(JFrame parent, Perspective perspective) {
super(parent);
this.perspective = perspective;
parentBounds = parent.getBounds();
initialize();
}
/**
* Set up the UI elements
*/
private void initialize() {
setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
JPanel mainPane = new JPanel(new BorderLayout());
mainPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
setContentPane(mainPane);
Box buttonBox = Box.createHorizontalBox();
buttonBox.add(Box.createHorizontalGlue());
buttonBox.add(saveButton);
buttonBox.add(Box.createHorizontalStrut(10));
buttonBox.add(closeButton);
buttonBox.setBorder(BorderFactory.createEmptyBorder(5, 5, 2, 2));
getContentPane().add(buttonBox, "South");
getContentPane().add(scrollPane, "Center");
editorPane.setFont(new Font("Monospaced", Font.PLAIN, 12));
scrollPane.setMinimumSize(new Dimension(600, 600));
scrollPane.setPreferredSize(new Dimension(600, 600));
scrollPane.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
saveButton.setAction(new AbstractAction(GemCutter.getResourceString("SD_Save")) {
private static final long serialVersionUID = 1925793734828203467L;
public void actionPerformed(ActionEvent evt) {
saveText();
}
});
closeButton.setAction(new AbstractAction(GemCutter.getResourceString("SD_Close")) {
private static final long serialVersionUID = 8848595347968035051L;
public void actionPerformed(ActionEvent evt) {
savePosition();
setVisible(false);
}
});
editorPane.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent evt) {
if(evt.getKeyCode() == KeyEvent.VK_S && (evt.isControlDown() || evt.isAltDown())) {
// Ignore Ctrl+S / Alt+S on files that cannot be edited
if(canSave()) {
saveText();
}
} else if(evt.getKeyChar() == KeyEvent.VK_C && evt.isAltDown()) {
savePosition();
setVisible(false);
} else if(evt.getKeyChar() != KeyEvent.CHAR_UNDEFINED) {
updateSaveButtonStatus();
setModifiedTitle(true);
}
}
});
addWindowListener(new WindowAdapter() {
@Override
public void windowActivated(WindowEvent e) {
// Force the focus to the editor component (rather than, say, the
// Close button) to ensure that the selection is visible.
editorPane.requestFocusInWindow();
}
@Override
public void windowClosing(WindowEvent e) {
savePosition();
}
});
pack();
setModal(false);
if(hasSavedPosition()) {
setPositionToSaved();
} else {
setLocation(parentBounds.x + parentBounds.width - getWidth(), parentBounds.y);
}
}
/**
* @return boolean True if current module is editable, false otherwise.
*/
private boolean canSave() {
return isEditable;
}
/** Enable the save button if the module is editable and changed,
* disable it if the module is not editable or unchanged */
private void updateSaveButtonStatus() {
saveButton.setEnabled(isEditable);
}
/** If the file is modified, include a star before the name. */
private void setModifiedTitle(boolean fileModified) {
if (!isEditable) {
setTitle(GemCutterMessages.getString("SD_SearchResultsTitleReadOnly", currentSourcePosition.getSourceName(), getModulePath()));
} else {
if (fileModified) {
setTitle(GemCutterMessages.getString("SD_SearchResultsTitleModified", currentSourcePosition.getSourceName(), getModulePath()));
} else {
setTitle(GemCutterMessages.getString("SD_SearchResultsTitle", currentSourcePosition.getSourceName(), getModulePath()));
}
}
}
/** Save changes to the file */
private void saveText() {
ModuleName moduleName = getModuleNameFromCurrentSourcePosition();
CALSourceManager sourceManager = perspective.getWorkspace().getSourceManager(moduleName);
Status saveStatus = new Status("Saving module text");
sourceManager.saveSource(moduleName, editorPane.getText(), saveStatus);
if(!saveStatus.isOK()) {
String errTitle = GemCutter.getResourceString("CannotSaveDialogTitle");
String errMessage = GemCutter.getResourceString("SaveModuleError");
JOptionPane.showMessageDialog(this, errMessage, errTitle, JOptionPane.ERROR_MESSAGE);
System.out.println(saveStatus.getDebugMessage());
return;
}
saveButton.setEnabled(false);
setModifiedTitle(false);
}
/**
* @return true if the module that contains the current source position is writeable,
* or false otherwise.
*/
private boolean isModuleWriteable() {
ModuleName moduleName = getModuleNameFromCurrentSourcePosition();
CALSourceManager sourceManager = perspective.getWorkspace().getSourceManager(moduleName);
return sourceManager.isWriteable(moduleName);
}
/**
* @return that describes the location of the module. This will normally
* be an absolute path to the file in the filesystem.
*/
private String getModulePath() {
ModuleName moduleName = getModuleNameFromCurrentSourcePosition();
CALFeatureName moduleFeatureName = CALFeatureName.getModuleFeatureName(moduleName);
ResourceName moduleResourceName = new ResourceName(moduleFeatureName);
ResourceStore sourceStore = perspective.getWorkspace().getSourceManager(moduleName).getResourceStore();
// If the source store doesn't know about this module, then we don't know how to find its file
if (!sourceStore.hasFeature(moduleResourceName)) {
return null;
}
if (sourceStore instanceof ResourcePathStore) {
ResourcePathStore sourcePathStore = (ResourcePathStore)sourceStore;
if (sourcePathStore instanceof ResourceNullaryStore) {
File currentFile = NullaryEnvironment.getNullaryEnvironment().getFile(sourcePathStore.getResourcePath(moduleResourceName), false);
if(currentFile != null) {
return currentFile.getAbsolutePath();
}
}
}
return null;
}
/**
* @return the module name as specified by the current source position.
*/
private ModuleName getModuleNameFromCurrentSourcePosition() {
return ModuleName.make(currentSourcePosition.getSourceName());
}
/**
* Sets the text and selection of the dialog based on a SourcePosition.
* @param searchResultItem Specification of the hit to highlight
*/
void setResult(SearchResultItem.Precise searchResultItem) {
SourcePosition sourcePosition = searchResultItem.getSourcePosition();
// Update text if necessary
String sourceName = sourcePosition.getSourceName();
if(currentSourcePosition == null || !currentSourcePosition.getSourceName().equals(sourceName)) {
Reader sourceReader = perspective.getWorkspace().getSourceDefinition(ModuleName.make(sourceName)).getSourceReader(new Status("reading source for search hit display"));
if (sourceReader == null) {
System.err.println("Could not read source definition for source: " + sourceName);
return;
}
sourceReader = new BufferedReader(sourceReader);
try {
editorPane.read(sourceReader, null);
} catch (IOException e) {
e.printStackTrace();
return;
} finally {
try {
sourceReader.close();
} catch (IOException e) {
}
}
}
currentSourcePosition = sourcePosition;
selectTargetAtCurrentPosition(searchResultItem.getSourceRange());
isEditable = isModuleWriteable();
String modulePath = getModulePath();
// Title depends on whether the module is readOnly
if(isEditable) {
setTitle(GemCutterMessages.getString("SD_SearchResultsTitle", sourcePosition.getSourceName(), modulePath));
} else if (modulePath != null) {
setTitle(GemCutterMessages.getString("SD_SearchResultsTitleReadOnly", sourcePosition.getSourceName(), modulePath));
} else {
setTitle(GemCutterMessages.getString("SD_SearchResultsTitleReadOnlyWithoutFile", sourcePosition.getSourceName()));
}
editorPane.setEditable(isEditable);
saveButton.setEnabled(false);
}
private void selectTargetAtCurrentPosition(SourceRange sourceRange) {
Document doc = editorPane.getDocument();
SourcePosition startPosition = sourceRange.getStartSourcePosition();
SourcePosition endPosition = sourceRange.getEndSourcePosition();
try {
int len = doc.getLength();
String text = doc.getText(0, len);
int startIndex = startPosition.getPosition(text);
int endIndex = endPosition.getPosition(text, startPosition, startIndex);
editorPane.select(startIndex, endIndex);
} catch (BadLocationException e1) {
e1.printStackTrace();
}
}
/**
* Save the dialog's position and size into the GemCutter's preferences.
*/
void savePosition() {
GemCutter.getPreferences().putInt(SEARCH_RESULTS_DIALOG_X_PREF_KEY, getX());
GemCutter.getPreferences().putInt(SEARCH_RESULTS_DIALOG_Y_PREF_KEY, getY());
GemCutter.getPreferences().putInt(SEARCH_RESULTS_DIALOG_WIDTH_PREF_KEY, getWidth());
GemCutter.getPreferences().putInt(SEARCH_RESULTS_DIALOG_HEIGHT_PREF_KEY, getHeight());
}
/**
* @return true if the preferences contain position and size info for this dialog
*/
boolean hasSavedPosition() {
return (GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_X_PREF_KEY, -1) != -1 &&
GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_Y_PREF_KEY, -1) != -1 &&
GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_WIDTH_PREF_KEY, -1) != -1 &&
GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_HEIGHT_PREF_KEY, -1) != -1);
}
/**
* Set the bounds of the dialog to the position and size stored in the preferences
*/
void setPositionToSaved() {
setBounds(GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_X_PREF_KEY, 0),
GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_Y_PREF_KEY, 0),
GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_WIDTH_PREF_KEY, 600),
GemCutter.getPreferences().getInt(SEARCH_RESULTS_DIALOG_HEIGHT_PREF_KEY, 800));
}
}
/**
* Typesafe enumeration representing the type of search to perform
*
* @author Jawright
*/
static final class SearchType {
static final SearchType ALL = new SearchType("AllOccurrences");
static final SearchType REFERENCES = new SearchType("References");
static final SearchType DEFINITION = new SearchType("Declarations");
static final SearchType INSTANCES = new SearchType("Instances");
static final SearchType CLASSES = new SearchType("Classes");
static final SearchType CONSTRUCTIONS = new SearchType("Constructions");
/** Name of this type of search */
private final String typeName;
/**
* @param typeName Name of the type of search to be represented
*/
private SearchType(String typeName) {
this.typeName = typeName;
}
/** @return a String representation of this value */
@Override
public String toString() {
return typeName;
}
/**
* @param key String representing a SearchType
* @return The SearchType that corresponds to key
*/
static final SearchType fromString(String key) {
if(key.equals(REFERENCES.typeName)) {
return REFERENCES;
} else if(key.equals(DEFINITION.typeName)) {
return DEFINITION;
} else if(key.equals(INSTANCES.typeName)) {
return INSTANCES;
} else if(key.equals(CLASSES.typeName)) {
return CLASSES;
} else if(key.equals(CONSTRUCTIONS.typeName)) {
return CONSTRUCTIONS;
} else if(key.equals(ALL.typeName)) {
return ALL;
} else {
throw new IllegalArgumentException("unrecognized SearchType string");
}
}
}
}