/*
* org.openmicroscopy.shoola.util.ui.QuickSearch
*
*------------------------------------------------------------------------------
* Copyright (C) 2006-2014 University of Dundee. All rights reserved.
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*------------------------------------------------------------------------------
*/
package org.openmicroscopy.shoola.agents.dataBrowser.view;
//Java imports
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.border.BevelBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
//Third-party libraries
import info.clearthought.layout.TableLayout;
//Application-internal dependencies
import org.jdesktop.swingx.JXBusyLabel;
import org.openmicroscopy.shoola.util.ui.IconManager;
import org.openmicroscopy.shoola.util.ui.UIUtilities;
import org.openmicroscopy.shoola.util.ui.search.SearchContextMenu;
import org.openmicroscopy.shoola.util.ui.search.SearchObject;
import org.openmicroscopy.shoola.util.ui.search.SearchUtil;
/**
* Basic panel with text field for searching.
*
* @author Jean-Marie Burel
* <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a>
* @author Donald MacDonald
* <a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a>
* @version 3.0
* <small>
* (<b>Internal version:</b> $Revision: $Date: $)
* </small>
* @since OME3.0
*/
public class QuickSearch
extends JPanel
implements ActionListener, DocumentListener, PropertyChangeListener
{
/** Indicates to search for tags. */
public static final int TAGS = 0;
/** Indicates to search for textual annotations. */
public static final int COMMENTS = 1;
/** Indicates to search for name. */
public static final int FULL_TEXT = 2;
/** Indicates to search for objects rated one or higher. */
public static final int RATED_ONE_OR_BETTER = 3;
/** Indicates to search for objects rated two or higher. */
public static final int RATED_TWO_OR_BETTER = 4;
/** Indicates to search for objects rated three or higher. */
public static final int RATED_THREE_OR_BETTER = 5;
/** Indicates to search for objects rated four or higher. */
public static final int RATED_FOUR_OR_BETTER = 6;
/** Indicates to search for objects rated five. */
public static final int RATED_FIVE = 7;
/** Indicates to search for objects unrated. */
public static final int UNRATED = 8;
/** Indicates to reset all nodes. */
public static final int SHOW_ALL = 9;
/** Indicates to search for the untagged nodes. */
public static final int UNTAGGED = 10;
/** Indicates to search for the uncommented nodes. */
public static final int UNCOMMENTED = 11;
/** Indicates to search for the tagged nodes. */
public static final int TAGGED = 12;
/** Indicates to search for the commented nodes. */
public static final int COMMENTED = 13;
/** Indicates to search for nodes with ROIs. */
public static final int HAS_ROIS = 14;
/** Indicates to search for nodes without ROIs. */
public static final int NO_ROIS = 15;
/** Indicates a customized search where there is no matching quick search option. */
public static final int NONE = 16;
/** Bound property indicating to search for given terms. */
public static final String QUICK_SEARCH_PROPERTY = "quickSearch";
/** Bound property indicating to search for given terms. */
public static final String VK_UP_SEARCH_PROPERTY = "vkUpSearch";
/** Bound property indicating to search for given terms. */
public static final String VK_DOWN_SEARCH_PROPERTY = "vkDownSearch";
/** Text displayed when the show all buttons is selected. */
private static final String SHOW_ALL_TEXT = "Show All ";
/** Text displayed when the show all buttons is selected. */
private static final String SHOW_ALL_DESCRIPTION = "filter ";
/** Removes the text from the text field. */
private static final int CLEAR = 0;
/** The selected node. */
private SearchObject selectedNode;
/** Area where to enter the tags to search. */
private JTextField searchArea;
/** Button to look nice. Does nothing for now. */
private JButton searchButton;
/** Button to clear the text. */
private JButton clearButton;
/** Button to clear the text. */
private JButton menuButton;
/** Status label. */
private JXBusyLabel status;
/** The Layout manager used to lay out the search area. */
private TableLayout layoutManager;
/** UI Component hosting the various items for the search. */
private JPanel searchPanel;
/** The popup menu. */
private SearchContextMenu menu;
/** The possible options. */
private List<SearchObject> nodes;
/** The possible options. */
private List<SearchObject> ratedNodes;
/** Label if any. */
private String label;
/** The node to show all the objects. */
private SearchObject showAll;
/** Tool bar hosting the clear functionalities. */
private JToolBar cleanBar;
/**
* Flag indicating that only one context can be selected at a time
* if set to <code>true</code>, more than one context if set
* to <code>false</code>.
*/
private boolean singleSelection;
/** The default text displayed when all elements are shown. */
private String defaultText;
/** Shows the menu. */
private void initMenu()
{
Rectangle r = searchPanel.getBounds();
if (menu == null) {
if (selectedNode != null)
menu = new SearchContextMenu(nodes, ratedNodes, r.width,
selectedNode, singleSelection);
else
menu = new SearchContextMenu(nodes, ratedNodes, r.width,
singleSelection);
menu.addPropertyChangeListener(
SearchContextMenu.SEARCH_CONTEXT_PROPERTY, this);
}
}
/** Initializes the components composing the display. */
private void initComponents()
{
defaultText = " images";
showAll = new SearchObject(SHOW_ALL, null, SHOW_ALL_TEXT+defaultText);
IconManager icons = IconManager.getInstance();
Icon icon = icons.getIcon(IconManager.CLEAR_DISABLED);
Dimension d = new Dimension(icon.getIconWidth(), icon.getIconHeight());
status = new JXBusyLabel(d);
UIUtilities.setTextAreaDefault(status);
status.setBorder(null);
clearButton = new JButton(icon);
//clearButton.setEnabled(false);
clearButton.setToolTipText("Clear Filtering and Show All");
UIUtilities.setTextAreaDefault(clearButton);
clearButton.setBorder(null);
clearButton.addActionListener(this);
clearButton.setActionCommand(""+CLEAR);
searchArea = new JTextField(15);
searchArea.setText(SHOW_ALL_DESCRIPTION+defaultText);
UIUtilities.setTextAreaDefault(searchArea);
searchArea.setBorder(null);
searchArea.getDocument().addDocumentListener(this);
searchArea.addKeyListener(new KeyAdapter() {
/** Finds the phrase. */
public void keyPressed(KeyEvent e)
{
Object source = e.getSource();
if (source != searchArea) return;
switch (e.getKeyCode()) {
case KeyEvent.VK_ENTER:
handleKeyEnter();
break;
case KeyEvent.VK_UP:
firePropertyChange(VK_UP_SEARCH_PROPERTY,
Boolean.valueOf(false), Boolean.valueOf(true));
break;
case KeyEvent.VK_DOWN:
firePropertyChange(VK_DOWN_SEARCH_PROPERTY,
Boolean.valueOf(false), Boolean.valueOf(true));
}
}
});
}
/**
* Initializes the components composing the display.
*
* @param icon The search icon.
*/
private void initComponents(Icon icon)
{
IconManager icons = IconManager.getInstance();
if (icon == null) icon = icons.getIcon(IconManager.SEARCH);
searchButton = new JButton(icon);
UIUtilities.setTextAreaDefault(searchButton);
searchButton.setBorder(null);
initComponents();
}
/**
* Initializes the menu and related components.
*
* @param nodes The nodes to display.
* @param ratedNodes The rated nodes if any.
*/
private void initSearchComponents(List<SearchObject> nodes,
List<SearchObject> ratedNodes)
{
if (ratedNodes == null) ratedNodes = new ArrayList<SearchObject>();
this.nodes = nodes;
this.ratedNodes = ratedNodes;
IconManager icons = IconManager.getInstance();
//check when to create it
menuButton = new JButton(icons.getIcon(IconManager.FILTER_MENU));
menuButton.setToolTipText("Display the filtering options.");
UIUtilities.setTextAreaDefault(menuButton);
menuButton.setBorder(null);
menuButton.addMouseListener(new MouseAdapter() {
/**
* Displays a menu with the available context.
* @see MouseAdapter#mousePressed(MouseEvent)
*/
public void mousePressed(MouseEvent e)
{
initMenu();
Rectangle r = searchPanel.getBounds();
menu.show(searchPanel, 0, r.height);
}
});
selectedNode = nodes.get(0);
searchButton = new JButton(selectedNode.getIcon());
//searchButton.setEnabled(false);
UIUtilities.setTextAreaDefault(searchButton);
searchButton.setBorder(null);
initComponents();
searchArea.setToolTipText(selectedNode.getDescription()+".");
}
/**
* Builds and lays out the UI.
*
* @param label The text to display in front of the text field.
*/
private void buildGUI(String label)
{
double w = 0;
if (menuButton != null) w = TableLayout.PREFERRED;
double[][] pl = {{TableLayout.PREFERRED, w, TableLayout.FILL,
TableLayout.PREFERRED},
{TableLayout.PREFERRED} }; //rows
layoutManager = new TableLayout(pl);
searchPanel = new JPanel();
UIUtilities.setTextAreaDefault(searchPanel);
searchPanel.setLayout(layoutManager);
searchPanel.setBorder(
BorderFactory.createBevelBorder(BevelBorder.LOWERED));
//searchPanel.add(searchButton, "0, 0, f, c");
if (menuButton != null) {
JToolBar bar = new JToolBar();
bar.setFloatable(false);
bar.setRollover(true);
bar.setBorder(null);
bar.add(menuButton);
searchPanel.add(bar, "1, 0, FULL, CENTER");
}
searchPanel.add(searchArea, "2, 0, FULL, CENTER");
if (clearButton != null) {
cleanBar = new JToolBar();
cleanBar.setFloatable(false);
cleanBar.setRollover(true);
cleanBar.setBorder(null);
cleanBar.add(clearButton);
cleanBar.setVisible(false);
searchPanel.add(cleanBar, "3, 0, FULL, CENTER");
}
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setOpaque(true);
if (label != null && label.trim().length() > 0)
add(UIUtilities.setTextFont(label));
add(searchPanel);
}
/**
* Displays the context of the search.
*
* @param oldNode The node previously selected.
*/
private void setSearchContext(SearchObject oldNode)
{
String text = "";
setSearchEnabled(false);
cleanBar.setVisible(false);
if(selectedNode!=null) {
switch (selectedNode.getIndex()) {
case RATED_ONE_OR_BETTER:
case RATED_TWO_OR_BETTER:
case RATED_THREE_OR_BETTER:
case RATED_FOUR_OR_BETTER:
case RATED_FIVE:
case UNRATED:
case UNTAGGED:
case UNCOMMENTED:
case TAGGED:
case COMMENTED:
case HAS_ROIS:
case NO_ROIS:
text = selectedNode.getDescription();
break;
case SHOW_ALL:
text = SHOW_ALL_DESCRIPTION+defaultText;
break;
case TAGS:
case COMMENTS:
case FULL_TEXT:
if (oldNode != null) {
int oldIndex = oldNode.getIndex();
if (oldIndex != TAGS && oldIndex != COMMENTS &&
oldIndex != FULL_TEXT) text = "";
}
setFocusOnArea();
setSearchEnabled(true);
}
}
searchArea.getDocument().removeDocumentListener(this);
searchArea.setText(text);
searchArea.getDocument().addDocumentListener(this);
}
/** Creates a new instance. */
public QuickSearch()
{
initComponents(null);
buildGUI(null);
}
/**
* Creates a new instance.
*
* @param icon The search icon.
*/
public QuickSearch(Icon icon)
{
this(null, icon);
}
/**
* Creates a new instance.
*
* @param label The text displayed in front of the panel.
*/
public QuickSearch(String label)
{
this.label = label;
initComponents(null);
buildGUI(label);
}
/**
* Creates a new instance.
*
* @param label The text displayed in front of the panel.
* @param icon The search icon.
*/
public QuickSearch(String label, Icon icon)
{
this.label = label;
initComponents(icon);
buildGUI(label);
}
/**
* Creates a new instance.
*
* @param nodes The nodes describing the context of the search.
* @param ratedNodes The rated nodes if any.
*/
public QuickSearch(List<SearchObject> nodes, List<SearchObject> ratedNodes)
{
this(null, nodes, ratedNodes);
}
/**
* Creates a new instance.
*
* @param label The text displayed in front of the panel.
* @param nodes The nodes describing the context of the search.
* @param ratedNodes The rated nodes if any.
*/
public QuickSearch(String label, List<SearchObject> nodes,
List<SearchObject> ratedNodes)
{
this.label = label;
if (nodes == null || nodes.size() == 0)
initComponents(null);
else initSearchComponents(nodes, ratedNodes);
buildGUI(label);
}
/** Selects the node. */
protected void onNodeSelection()
{
List<String> l = SearchUtil.splitTerms(searchArea.getText(),
SearchUtil.COMMA_SEPARATOR);
if (selectedNode == null) return;
//if (selectedNode == null) selectedNode = showAll;
selectedNode.setResult(l);
firePropertyChange(QUICK_SEARCH_PROPERTY, null, selectedNode);
}
/**
* Sets to <code>true</code> if only one context can be selected at a time
* to <code>false</code> otherwise.
*
* @param singleSelection The value to set.
*/
public void setSingleSelection(boolean singleSelection)
{
this.singleSelection = singleSelection;
}
/**
* Adds the default searching context.
*
* @param text The default text.
*/
public void setDefaultSearchContext(String text)
{
if (text != null) defaultText = text;
List<SearchObject> nodes = new ArrayList<SearchObject>();
SearchObject node = new SearchObject(SHOW_ALL, null,
SHOW_ALL_TEXT+defaultText);
nodes.add(node);
node = new SearchObject(FULL_TEXT, null,
SearchComponent.NAME_TEXT);
nodes.add(node);
node = new SearchObject(TAGS, null, SearchComponent.NAME_TAGS);
nodes.add(node);
node = new SearchObject(COMMENTS, null, SearchComponent.NAME_COMMENTS);
nodes.add(node);
node = new SearchObject(TAGGED, null, SearchComponent.TAGGED_TEXT);
nodes.add(node);
node = new SearchObject(UNTAGGED, null, SearchComponent.UNTAGGED_TEXT);
nodes.add(node);
node = new SearchObject(COMMENTED, null,
SearchComponent.COMMENTED_TEXT);
nodes.add(node);
node = new SearchObject(UNCOMMENTED, null,
SearchComponent.UNCOMMENTED_TEXT);
nodes.add(node);
node = new SearchObject(HAS_ROIS, null,
SearchComponent.HAS_ROIS_TEXT);
nodes.add(node);
node = new SearchObject(NO_ROIS, null,
SearchComponent.NO_ROIS_TEXT);
nodes.add(node);
List<SearchObject> ratedNodes = new ArrayList<SearchObject>();
node = new SearchObject(RATED_ONE_OR_BETTER, null, "* or better");
ratedNodes.add(node);
node = new SearchObject(RATED_TWO_OR_BETTER, null, "** or better");
ratedNodes.add(node);
node = new SearchObject(RATED_THREE_OR_BETTER, null, "*** or better");
ratedNodes.add(node);
node = new SearchObject(RATED_FOUR_OR_BETTER, null, "**** or better");
ratedNodes.add(node);
node = new SearchObject(RATED_FIVE, null, "*****");
ratedNodes.add(node);
node = new SearchObject(UNRATED, null, "Unrated");
ratedNodes.add(node);
initSearchComponents(nodes, ratedNodes);
removeAll();
buildGUI(label);
}
/**
* Sets the default text.
*
* @param defaultText The value to set.
*/
public void setDefaultText(String defaultText)
{
if (defaultText == null) return;
this.defaultText = defaultText;
searchArea.setText(SHOW_ALL_DESCRIPTION+defaultText);
showAll.setDescription(SHOW_ALL_TEXT+defaultText);
}
/**
* Returns the selected node.
*
* @return See above.
*/
public SearchObject getSelectedNode() { return selectedNode; }
/**
* Returns the search area.
*
* @return See above.
*/
public JComponent getSelectionArea() { return searchPanel; }
/** Sets the focus on the {@link #searchArea}. */
public void setFocusOnArea()
{
searchArea.requestFocusInWindow();
String v = getSearchValue();
int l = v.length();
searchArea.setCaretPosition(l);
searchArea.moveCaretPosition(l);
}
/**
* Returns the text of the {@link #searchArea}.
*
* @return See above.
*/
public String getSearchValue() { return searchArea.getText(); }
/**
* Sets the value of the {@link #searchArea}.
*
* @param text The value to set.
* @param removeLast Pass <code>true</code> to remove the last item,
* <code>false</code> otherwise.
*/
public void setSearchValue(String text, boolean removeLast)
{
if (text == null) return;
List<String> l = SearchUtil.splitTerms(getSearchValue(),
SearchUtil.COMMA_SEPARATOR);
if (removeLast && l.size() > 0) l.remove(l.size()-1);
String result = SearchUtil.formatString(text, l);
searchArea.getDocument().removeDocumentListener(this);
searchArea.setText(result);
searchArea.getDocument().addDocumentListener(this);
}
/**
* Sets the enabled flag of the {@link #searchArea}.
*
* @param enabled The value to set.
*/
public void setSearchEnabled(boolean enabled)
{
//searchArea.setEnabled(enabled);
searchArea.setEditable(enabled);
}
/**
* Sets the value of the {@link #searchArea}.
*
* @param text The value to set.
* @param removeLast Pass <code>true</code> to remove the last item,
* <code>false</code> otherwise.
*/
public void setSearchValue(List<String> text, boolean removeLast)
{
if (text == null || text.size() == 0) return;
List<String> l = SearchUtil.splitTerms(getSearchValue(),
SearchUtil.COMMA_SEPARATOR);
Iterator<String> i = text.iterator();
String value;
List<String> values = new ArrayList<String>();
StringBuffer term = new StringBuffer();
int index = 0;
int n = text.size()-1;
while (i.hasNext()) {
value = i.next();
if (value != null) {
value = value.trim();
if (!l.contains(value))
values.add(value);
else {
term.append(value);
if (index < n) {
term.append(SearchUtil.COMMA_SEPARATOR);
term.append(SearchUtil.SPACE_SEPARATOR);
}
index++;
}
}
}
i = values.iterator();
index = 0;
while (i.hasNext()) {
term.append(i.next());
if (index < n) {
term.append(SearchUtil.COMMA_SEPARATOR);
term.append(SearchUtil.SPACE_SEPARATOR);
}
index++;
}
searchArea.getDocument().removeDocumentListener(this);
searchArea.setText(term.toString());
searchArea.getDocument().addDocumentListener(this);
//setSearchValue(term, removeLast);
}
/**
* Determines the context corresponding to the passed index.
*
* @param index The value to handle.
*/
public void setSearchContext(int index)
{
Iterator<SearchObject> i = nodes.iterator();
SearchObject node;
SearchObject selectedNode = null;
while (i.hasNext()) {
node = i.next();
if (node.getIndex() == index) {
selectedNode = node;
break;
}
}
if (selectedNode == null) {
i = ratedNodes.iterator();
while (i.hasNext()) {
node = i.next();
if (node.getIndex() == index) {
selectedNode = node;
break;
}
}
}
setFilteringStatus(true);
initMenu();
menu.setSelectedNode(selectedNode);
this.selectedNode = selectedNode;
setSearchContext(null);
}
/**
* Replaces the clear button and shows the tool bar if the passed
* value is <code>true</code>. Otherwise add the clear button and
* shows or hides the bar depending on the selected node.
*
* @param busy See above.
*/
public void setFilteringStatus(boolean busy)
{
status.setBusy(busy);
cleanBar.removeAll();
if (busy) {
cleanBar.add(status);
cleanBar.setVisible(true);
setFocusOnArea();
} else {
cleanBar.add(clearButton);
boolean visible = false;
if (selectedNode != null) {
switch (selectedNode.getIndex()) {
case TAGS:
case COMMENTS:
case FULL_TEXT:
String text = searchArea.getText();
if (text != null && text.trim().length() == 0)
visible = true;
}
}
cleanBar.setVisible(visible);
setFocusOnArea();
}
}
/** Removes the text from the display. */
public void clear()
{
searchArea.getDocument().removeDocumentListener(this);
searchArea.setText("");
searchArea.getDocument().addDocumentListener(this);
//layoutManager.setColumn(3, 0);
searchPanel.validate();
searchPanel.repaint();
cleanBar.setVisible(false);
/*
switch (selectedNode.getIndex()) {
case RATED_ONE_OR_BETTER:
case RATED_TWO_OR_BETTER:
case RATED_THREE_OR_BETTER:
case RATED_FOUR_OR_BETTER:
case UNRATED:
case UNTAGGED:
case UNCOMMENTED:
case TAGGED:
case COMMENTED:
setSearchContext(SHOW_ALL);
}
firePropertyChange(QUICK_SEARCH_PROPERTY, null, showAll);
*/
}
/**
* Class extending this class should override that method for
* code completion.
*/
protected void handleTextInsert() {}
/**
* Class extending this class should override that method for
* code completion.
*/
protected void handleKeyEnter() {}
/**
* Shows or hides the {@link #clearButton} when some text is entered.
* @see DocumentListener#insertUpdate(DocumentEvent)
*/
public void insertUpdate(DocumentEvent e)
{
try {
String s = e.getDocument().getText(e.getOffset(), e.getLength());
if (SearchUtil.COMMA_SEPARATOR.equals(s) ||
SearchUtil.SPACE_SEPARATOR.equals(s))
return;
} catch (Exception ex) {
//ignore
}
handleTextInsert();
cleanBar.setVisible(true);
searchPanel.validate();
searchPanel.repaint();
}
/**
* Shows or hides the {@link #clearButton} when some text is entered.
* @see DocumentListener#removeUpdate(DocumentEvent)
*/
public void removeUpdate(DocumentEvent e)
{
//if (e.getDocument().getLength() == 0) clear();
String text = searchArea.getText();
if (text == null || text.trim().length() == 0)
cleanBar.setVisible(false);
else handleTextInsert();
}
/** Subclasses should override this method to handle the search. */
public void search() {}
/**
* Clears the text from the display.
* @see ActionListener#actionPerformed(ActionEvent)
*/
public void actionPerformed(ActionEvent e)
{
int index = Integer.parseInt(e.getActionCommand());
switch (index) {
case CLEAR:
clear();
break;
}
}
/**
* Reacts to the property fired by the <code>SearchContextMenu</code>
* @see PropertyChangeListener#propertyChange(PropertyChangeEvent)
*/
public void propertyChange(PropertyChangeEvent evt)
{
String name = evt.getPropertyName();
if (SearchContextMenu.SEARCH_CONTEXT_PROPERTY.equals(name)) {
SearchObject node = (SearchObject) evt.getNewValue();
SearchObject oldNode = selectedNode;
if (node != null) selectedNode = node;
searchButton.setIcon(selectedNode.getIcon());
UIUtilities.setTextAreaDefault(searchButton);
searchButton.setBorder(null);
searchArea.setToolTipText(selectedNode.getDescription());
setSearchContext(oldNode);
onNodeSelection();
} else if (QUICK_SEARCH_PROPERTY.equals(name)) search();
}
/**
* Required by I/F but no-operation implementation in our case.
* @see DocumentListener#changedUpdate(DocumentEvent)
*/
public void changedUpdate(DocumentEvent e) {}
}