/******************************************************************************* | |
* Copyright (C) 2009 The University of Manchester | |
* | |
* Modifications to the initial code base are copyright of their respective | |
* authors, or their employers as appropriate. | |
* | |
* This program is free software; you can redistribute it and/or modify it under | |
* the terms of the GNU Lesser General Public License as published by the Free | |
* Software Foundation; either version 2.1 of the License, or (at your option) | |
* any later version. | |
* | |
* This program is distributed in the hope that it will be useful, but WITHOUT | |
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more | |
* details. | |
* | |
* You should have received a copy of the GNU Lesser General Public License | |
* along with this program; if not, write to the Free Software Foundation, Inc., | |
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 | |
******************************************************************************/ | |
package org.apache.taverna.ui.perspectives.myexperiment; | |
import java.awt.BorderLayout; | |
import java.awt.Dimension; | |
import java.awt.GridBagConstraints; | |
import java.awt.GridBagLayout; | |
import java.awt.event.ActionEvent; | |
import java.awt.event.ActionListener; | |
import java.text.ParseException; | |
import java.util.Collections; | |
import java.util.LinkedList; | |
import java.util.List; | |
import java.util.Vector; | |
import javax.swing.BorderFactory; | |
import javax.swing.ImageIcon; | |
import javax.swing.JLabel; | |
import javax.swing.JOptionPane; | |
import javax.swing.JPanel; | |
import javax.swing.JScrollPane; | |
import javax.swing.JSplitPane; | |
import javax.swing.SwingUtilities; | |
import org.apache.taverna.ui.perspectives.myexperiment.model.Base64; | |
import org.apache.taverna.ui.perspectives.myexperiment.model.MyExperimentClient; | |
import org.apache.taverna.ui.perspectives.myexperiment.model.SearchEngine; | |
import org.apache.taverna.ui.perspectives.myexperiment.model.Util; | |
import org.apache.taverna.ui.perspectives.myexperiment.model.SearchEngine.QuerySearchInstance; | |
import org.apache.taverna.workbench.icons.WorkbenchIcons; | |
import org.apache.log4j.Logger; | |
/** | |
* @author Sergejs Aleksejevs | |
*/ | |
public class SearchTabContentPanel extends JPanel implements ActionListener { | |
// CONSTANTS | |
private final static int SEARCH_HISTORY_LENGTH = 50; | |
private final static int SEARCH_FAVOURITES_LENGTH = 30; | |
protected final static String SEARCH_FROM_FAVOURITES = "searchFromFavourites"; | |
protected final static String SEARCH_FROM_HISTORY = "searchFromHistory"; | |
protected final static String ADD_FAVOURITE_SEARCH_INSTANCE = "addFavouriteSearchInstance"; | |
protected final static String REMOVE_FAVOURITE_SEARCH_INSTANCE = "removeFavouriteSearchInstance"; | |
private final MainComponent pluginMainComponent; | |
private final MyExperimentClient myExperimentClient; | |
private final Logger logger; | |
// COMPONENTS | |
private JSplitPane spMainSplitPane; | |
private SearchOptionsPanel jpSearchOptions; | |
private JPanel jpFavouriteSearches; | |
private JPanel jpSearchHistory; | |
private SearchResultsPanel jpSearchResults; | |
private final ImageIcon iconFavourite = new ImageIcon(MyExperimentPerspective.getLocalResourceURL("favourite_icon")); | |
private final ImageIcon iconRemove = new ImageIcon(MyExperimentPerspective.getLocalResourceURL("destroy_icon")); | |
// Data storage | |
private QuerySearchInstance siPreviousSearch; | |
private LinkedList<QuerySearchInstance> llFavouriteSearches; | |
private LinkedList<QuerySearchInstance> llSearchHistory; | |
// Search components | |
private final SearchEngine searchEngine; // The search engine for executing keyword query searches | |
private final Vector<Long> vCurrentSearchThreadID; // This will keep ID of the current search thread (there will only be one such thread) | |
public SearchTabContentPanel(MainComponent component, MyExperimentClient client, Logger logger) { | |
super(); | |
// set main variables to ensure access to myExperiment, logger and the parent component | |
this.pluginMainComponent = component; | |
this.myExperimentClient = client; | |
this.logger = logger; | |
// initialise the favourite searches | |
String strFavouriteSearches = (String) myExperimentClient.getSettings().get(MyExperimentClient.INI_FAVOURITE_SEARCHES); | |
if (strFavouriteSearches != null) { | |
Object oFavouriteSearches = Base64.decodeToObject(strFavouriteSearches); | |
this.llFavouriteSearches = (LinkedList<QuerySearchInstance>) oFavouriteSearches; | |
} else { | |
this.llFavouriteSearches = new LinkedList<QuerySearchInstance>(); | |
} | |
// initialise the search history | |
String strSearchHistory = (String) myExperimentClient.getSettings().get(MyExperimentClient.INI_SEARCH_HISTORY); | |
if (strSearchHistory != null) { | |
Object oSearchHistory = Base64.decodeToObject(strSearchHistory); | |
this.llSearchHistory = (LinkedList<QuerySearchInstance>) oSearchHistory; | |
} else { | |
this.llSearchHistory = new LinkedList<QuerySearchInstance>(); | |
} | |
this.initialiseUI(); | |
this.updateFavouriteSearches(); | |
this.updateSearchHistory(); | |
// initialise the search engine | |
vCurrentSearchThreadID = new Vector<Long>(1); | |
vCurrentSearchThreadID.add(null); // this is just a placeholder, so that it's possible to update this value instead of adding new ones later | |
this.searchEngine = new SearchEngine(vCurrentSearchThreadID, false, jpSearchResults, pluginMainComponent, myExperimentClient, logger); | |
SwingUtilities.invokeLater(new Runnable() { | |
public void run() { | |
// THIS MIGHT NOT BE NEEDED AS THE SEARCH OPTIONS BOX NOW | |
// SETS THE MINIMUM SIZE OF THE SIDEBAR PROPERLY | |
spMainSplitPane.setDividerLocation(390); | |
spMainSplitPane.setOneTouchExpandable(true); | |
spMainSplitPane.setDoubleBuffered(true); | |
} | |
}); | |
} | |
private void initialiseUI() { | |
// create search options panel | |
jpSearchOptions = new SearchOptionsPanel(this, this.pluginMainComponent, this.myExperimentClient, this.logger); | |
jpSearchOptions.setMaximumSize(new Dimension(1024, 0)); // HACK: this is to make sure that search options box won't be stretched | |
// create favourite searches panel | |
jpFavouriteSearches = new JPanel(); | |
jpFavouriteSearches.setMaximumSize(new Dimension(1024, 0)); // HACK: this is to make sure that favourite searches box won't be stretched | |
jpFavouriteSearches.setLayout(new GridBagLayout()); | |
jpFavouriteSearches.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), " Favourite Searches "), BorderFactory.createEmptyBorder(0, 5, 5, 5))); | |
// create search history panel | |
jpSearchHistory = new JPanel(); | |
jpSearchHistory.setMaximumSize(new Dimension(1024, 0)); // HACK: this is to make sure that search history box won't be stretched | |
jpSearchHistory.setLayout(new GridBagLayout()); | |
jpSearchHistory.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), " Search History "), BorderFactory.createEmptyBorder(0, 5, 5, 5))); | |
// create the search sidebar | |
JPanel jpSearchSidebar = new JPanel(); | |
jpSearchSidebar.setLayout(new GridBagLayout()); | |
GridBagConstraints gbConstraints = new GridBagConstraints(); | |
gbConstraints.anchor = GridBagConstraints.NORTHWEST; | |
gbConstraints.fill = GridBagConstraints.BOTH; | |
gbConstraints.weightx = 1; | |
gbConstraints.gridx = 0; | |
gbConstraints.gridy = 0; | |
jpSearchSidebar.add(jpSearchOptions, gbConstraints); | |
gbConstraints.gridy = 1; | |
jpSearchSidebar.add(jpFavouriteSearches, gbConstraints); | |
gbConstraints.gridy = 2; | |
jpSearchSidebar.add(jpSearchHistory, gbConstraints); | |
JPanel jpSidebarContainer = new JPanel(); | |
jpSidebarContainer.setLayout(new BorderLayout()); | |
jpSidebarContainer.add(jpSearchSidebar, BorderLayout.NORTH); | |
// wrap sidebar in a scroll pane | |
JScrollPane spSearchSidebar = new JScrollPane(jpSidebarContainer); | |
spSearchSidebar.getVerticalScrollBar().setUnitIncrement(ResourcePreviewBrowser.PREFERRED_SCROLL); | |
spSearchSidebar.setMinimumSize(new Dimension(jpSidebarContainer.getMinimumSize().width + 20, 0)); | |
// create panel for search results | |
this.jpSearchResults = new SearchResultsPanel(this, pluginMainComponent, myExperimentClient, logger); | |
spMainSplitPane = new JSplitPane(); | |
spMainSplitPane.setLeftComponent(spSearchSidebar); | |
spMainSplitPane.setRightComponent(jpSearchResults); | |
// PUT EVERYTHING TOGETHER | |
this.setLayout(new BorderLayout()); | |
this.add(spMainSplitPane); | |
} | |
private void addToSearchListQueue(LinkedList<QuerySearchInstance> searchInstanceList, QuerySearchInstance searchInstanceToAdd, int queueSize) { | |
// check if such entry is already in the list | |
int iDuplicateIdx = searchInstanceList.indexOf(searchInstanceToAdd); | |
// only do the following if the new search instance list OR current instance is not the same | |
// as the last one in the list | |
if (searchInstanceList.size() == 0 | |
|| iDuplicateIdx != searchInstanceList.size() - 1) { | |
// if the current item is already in the list, remove it (then re-add at the end of the list) | |
if (iDuplicateIdx >= 0) | |
searchInstanceList.remove(iDuplicateIdx); | |
// we want to keep the history size constant, therefore when it reaches a certain | |
// size, oldest element needs to be removed | |
if (searchInstanceList.size() >= queueSize) | |
searchInstanceList.remove(); | |
// in either case, add the new element to the tail of the search history | |
searchInstanceList.offer(searchInstanceToAdd); | |
} | |
} | |
private void addToFavouriteSearches(QuerySearchInstance searchInstance) { | |
this.addToSearchListQueue(this.llFavouriteSearches, searchInstance, SEARCH_FAVOURITES_LENGTH); | |
Collections.sort(this.llFavouriteSearches); | |
} | |
// the method to update search history listing | |
protected void updateFavouriteSearches() { | |
this.jpFavouriteSearches.removeAll(); | |
if (this.llFavouriteSearches.size() == 0) { | |
GridBagConstraints c = new GridBagConstraints(); | |
c.weightx = 1.0; | |
c.anchor = GridBagConstraints.WEST; | |
this.jpFavouriteSearches.add(Util.generateNoneTextLabel("No favourite searches"), c); | |
} else { | |
for (int i = this.llFavouriteSearches.size() - 1; i >= 0; i--) { | |
addEntryToSearchListingPanel(this.llFavouriteSearches, i, SEARCH_FROM_FAVOURITES, this.jpFavouriteSearches, this.iconRemove, REMOVE_FAVOURITE_SEARCH_INSTANCE, "<html>Click to remove from your local favourite searches.<br>" | |
+ "(This will not affect your myExperiment profile settings.)</html>"); | |
} | |
} | |
this.jpFavouriteSearches.repaint(); | |
this.jpFavouriteSearches.revalidate(); | |
} | |
private void addToSearchHistory(QuerySearchInstance searchInstance) { | |
this.addToSearchListQueue(this.llSearchHistory, searchInstance, SEARCH_HISTORY_LENGTH); | |
} | |
// the method to update search history listing | |
protected void updateSearchHistory() { | |
this.jpSearchHistory.removeAll(); | |
if (this.llSearchHistory.size() == 0) { | |
GridBagConstraints c = new GridBagConstraints(); | |
c.weightx = 1.0; | |
c.anchor = GridBagConstraints.WEST; | |
this.jpSearchHistory.add(Util.generateNoneTextLabel(SearchResultsPanel.NO_SEARCHES_STATUS), c); | |
} else { | |
for (int i = this.llSearchHistory.size() - 1; i >= 0; i--) { | |
addEntryToSearchListingPanel(this.llSearchHistory, i, SEARCH_FROM_HISTORY, this.jpSearchHistory, this.iconFavourite, ADD_FAVOURITE_SEARCH_INSTANCE, "<html>Click to add to your local favourite" | |
+ " searches - these will be available every time you use Taverna.<br>(This will not affect your" | |
+ " myExperiment profile settings.)</html>"); | |
} | |
} | |
this.jpSearchHistory.repaint(); | |
this.jpSearchHistory.revalidate(); | |
// also update search history in History tab | |
if (this.pluginMainComponent.getHistoryBrowser() != null) { | |
this.pluginMainComponent.getHistoryBrowser().refreshSearchHistory(); | |
} | |
} | |
private void addEntryToSearchListingPanel(List<QuerySearchInstance> searchInstanceList, int iIndex, String searchAction, JPanel panelToPopulate, ImageIcon entryIcon, String iconAction, String iconActionTooltip) { | |
// labels with search query and search settings | |
JClickableLabel jclCurrentEntryLabel = new JClickableLabel(searchInstanceList.get(iIndex).getSearchQuery(), searchAction | |
+ ":" + iIndex, this, WorkbenchIcons.findIcon, SwingUtilities.LEFT, searchInstanceList.get(iIndex).toString()); | |
JLabel jlCurrentEntrySettings = new JLabel(searchInstanceList.get(iIndex).detailsAsString()); | |
jlCurrentEntrySettings.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0)); | |
// grouping search details and search settings together | |
JPanel jpCurentEntryDetails = new JPanel(); | |
jpCurentEntryDetails.setLayout(new GridBagLayout()); | |
GridBagConstraints c = new GridBagConstraints(); | |
c.anchor = GridBagConstraints.WEST; | |
jpCurentEntryDetails.add(jclCurrentEntryLabel, c); | |
c.weightx = 1.0; | |
jpCurentEntryDetails.add(jlCurrentEntrySettings, c); | |
// creating a "button" to add current item to favourites | |
JClickableLabel jclFavourite = new JClickableLabel("", iconAction + ":" | |
+ iIndex, this, entryIcon, SwingUtilities.LEFT, iconActionTooltip); | |
jclFavourite.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0)); | |
// putting all pieces of current item together | |
JPanel jpCurrentEntry = new JPanel(); | |
jpCurrentEntry.setLayout(new GridBagLayout()); | |
c.anchor = GridBagConstraints.WEST; | |
c.weightx = 1.0; | |
jpCurrentEntry.add(jpCurentEntryDetails, c); | |
c.anchor = GridBagConstraints.WEST; | |
c.weightx = 0; | |
jpCurrentEntry.add(jclFavourite, c); | |
// adding current item to the history list | |
c.fill = GridBagConstraints.HORIZONTAL; | |
c.weightx = 1.0; | |
c.gridx = 0; | |
c.gridy = GridBagConstraints.RELATIVE; | |
panelToPopulate.add(jpCurrentEntry, c); | |
} | |
public void actionPerformed(ActionEvent e) { | |
if (e.getSource().equals(this.jpSearchOptions.bSearch)) { | |
// "Search" button was clicked | |
// if no search query is specified, display error message | |
if (jpSearchOptions.getSearchQuery().length() == 0) { | |
javax.swing.JOptionPane.showMessageDialog(null, "Search query is empty. Please specify your search query and try again.", "Error", JOptionPane.WARNING_MESSAGE); | |
jpSearchOptions.focusSearchQueryField(); | |
} else { | |
// will ensure that if the value in the search result limit editor | |
// is invalid, it is processed properly | |
try { | |
this.jpSearchOptions.jsResultLimit.commitEdit(); | |
} catch (ParseException ex) { | |
JOptionPane.showMessageDialog(null, "Invalid search result limit value. This should be an\n" | |
+ "integer in the range of " | |
+ SearchOptionsPanel.SEARCH_RESULT_LIMIT_MIN | |
+ ".." | |
+ SearchOptionsPanel.SEARCH_RESULT_LIMIT_MAX, "MyExperiment Plugin - Error", JOptionPane.WARNING_MESSAGE); | |
this.jpSearchOptions.tfResultLimitTextField.selectAll(); | |
this.jpSearchOptions.tfResultLimitTextField.requestFocusInWindow(); | |
return; | |
} | |
// all fine, settings present - store the settings.. | |
siPreviousSearch = new SearchEngine.QuerySearchInstance(jpSearchOptions.getSearchQuery(), jpSearchOptions.getResultCountLimit(), jpSearchOptions.getSearchWorkflows(), jpSearchOptions.getSearchFiles(), jpSearchOptions.getSearchPacks(), jpSearchOptions.getSearchUsers(), jpSearchOptions.getSearchGroups()); | |
// .. and execute the query | |
this.jpSearchOptions.focusSearchQueryField(); | |
this.runSearch(); | |
} | |
} else if (e.getSource() instanceof JClickableLabel) { | |
if (e.getActionCommand().startsWith(SEARCH_FROM_HISTORY) | |
|| e.getActionCommand().startsWith(SEARCH_FROM_FAVOURITES)) { | |
// the part of the action command that is following the prefix is the ID in the search history / favourites storage; | |
// this search instance is removed from history and will be re-added at the top of it when search is launched | |
int iEntryID = Integer.parseInt(e.getActionCommand().substring(e.getActionCommand().indexOf(":") + 1)); | |
final QuerySearchInstance si = (e.getActionCommand().startsWith(SEARCH_FROM_HISTORY) ? this.llSearchHistory.remove(iEntryID) : this.llFavouriteSearches.get(iEntryID)); // in case of favourites, no need to remove the entry | |
// re-set search options in the settings box and re-run the search | |
SwingUtilities.invokeLater(new Runnable() { | |
public void run() { | |
jpSearchOptions.setSearchQuery(si.getSearchQuery()); | |
jpSearchOptions.setSearchAllResourceTypes(false); // reset the 'all resource types' | |
jpSearchOptions.setSearchWorkflows(si.getSearchWorkflows()); | |
jpSearchOptions.setSearchFiles(si.getSearchFiles()); | |
jpSearchOptions.setSearchPacks(si.getSearchPacks()); | |
jpSearchOptions.setSearchUsers(si.getSearchUsers()); | |
jpSearchOptions.setSearchGroups(si.getSearchGroups()); | |
jpSearchOptions.setResultCountLimit(si.getResultCountLimit()); | |
// set this as the 'latest' search | |
siPreviousSearch = si; | |
// run the search (and update the search history) | |
runSearch(); | |
} | |
}); | |
} else if (e.getActionCommand().startsWith(ADD_FAVOURITE_SEARCH_INSTANCE)) { | |
// get the ID of the entry in the history listing first; then fetch the instance itself | |
int iHistID = Integer.parseInt(e.getActionCommand().substring(e.getActionCommand().indexOf(":") + 1)); | |
final QuerySearchInstance si = this.llSearchHistory.get(iHistID); | |
// add item to favourites and re-draw the panel | |
SwingUtilities.invokeLater(new Runnable() { | |
public void run() { | |
addToFavouriteSearches(si); | |
updateFavouriteSearches(); | |
} | |
}); | |
} else if (e.getActionCommand().startsWith(REMOVE_FAVOURITE_SEARCH_INSTANCE)) { | |
// get the ID of the entry in the favourite searches listing first; then remove the instance with that ID from the list | |
int iFavouriteID = Integer.parseInt(e.getActionCommand().substring(e.getActionCommand().indexOf(":") + 1)); | |
this.llFavouriteSearches.remove(iFavouriteID); | |
// item removed from favourites - re-draw the panel now | |
SwingUtilities.invokeLater(new Runnable() { | |
public void run() { | |
updateFavouriteSearches(); | |
} | |
}); | |
} | |
} else if (e.getSource().equals(this.jpSearchResults.bRefresh)) { | |
// "Refresh" button clicked; disable clearing results and re-run previous search | |
this.jpSearchResults.bClear.setEnabled(false); | |
this.rerunLastSearch(); | |
} else if (e.getSource().equals(this.jpSearchResults.bClear)) { | |
// "Clear" button clicked - clear last search and disable re-running it | |
siPreviousSearch = null; | |
vCurrentSearchThreadID.set(0, null); | |
this.jpSearchResults.clear(); | |
this.jpSearchResults.setStatus(SearchResultsPanel.NO_SEARCHES_STATUS); | |
this.jpSearchResults.bClear.setEnabled(false); | |
this.jpSearchResults.bRefresh.setEnabled(false); | |
} | |
} | |
// search history will get updated by default | |
private void runSearch() { | |
runSearch(true); | |
} | |
// makes preparations and runs the current search | |
// (has an option not to update the search history) | |
private void runSearch(boolean bUpdateHistory) { | |
if (bUpdateHistory) { | |
// add current search to search history .. | |
this.addToSearchHistory(siPreviousSearch); | |
// .. update the search history box .. | |
SwingUtilities.invokeLater(new Runnable() { | |
public void run() { | |
updateSearchHistory(); | |
} | |
}); | |
} | |
// ..and run the query | |
this.searchEngine.searchAndPopulateResults(siPreviousSearch); | |
} | |
// re-executes the search for the most recent query | |
// (if searches have already been done before or were not cleared) | |
public void rerunLastSearch() { | |
if (this.siPreviousSearch != null) { | |
runSearch(); | |
} | |
} | |
public SearchResultsPanel getSearchResultPanel() { | |
return (this.jpSearchResults); | |
} | |
public LinkedList<QuerySearchInstance> getSearchFavouritesList() { | |
return (this.llFavouriteSearches); | |
} | |
public LinkedList<QuerySearchInstance> getSearchHistory() { | |
return (this.llSearchHistory); | |
} | |
} |