package prefuse.util.ui; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.Document; import prefuse.Visualization; import prefuse.data.Tuple; import prefuse.data.event.TupleSetListener; import prefuse.data.search.PrefixSearchTupleSet; import prefuse.data.search.SearchTupleSet; import prefuse.data.tuple.TupleSet; import prefuse.util.ColorLib; /** * Swing component that enables keyword search over prefuse data tuples. * * @author <a href="http://jheer.org">jeffrey heer</a> * @see prefuse.data.query.SearchQueryBinding */ public class JSearchPanel extends JPanel implements DocumentListener, ActionListener { private Object m_lock; private SearchTupleSet m_searcher; private JTextField m_queryF = new JTextField(15); private JLabel m_resultL = new JLabel(" "); private JLabel m_searchL = new JLabel("search >> "); private Box m_sbox = new Box(BoxLayout.X_AXIS); private String[] m_fields; private Color m_cancelColor = ColorLib.getColor(255,75,75); private boolean m_includeHitCount = false; private boolean m_monitorKeys = false; private boolean m_autoIndex = true; private boolean m_showBorder = true; private boolean m_showCancel = true; // ------------------------------------------------------------------------ // Free form constructors /** * Create a new JSearchPanel. * @param search the search tuple set conducting the searches * @param field the data field being searched */ public JSearchPanel(SearchTupleSet search, String field) { this(search, field, false); } /** * Create a new JSearchPanel. * @param search the search tuple set conducting the searches * @param field the data field being searched * @param monitorKeystrokes indicates if each keystroke event should result * in a new search being issued (true) or if searches should only be * initiated by hitting the enter key (false) */ public JSearchPanel(SearchTupleSet search, String field, boolean monitorKeystrokes) { this(null, search, new String[] {field}, false, monitorKeystrokes); } /** * Create a new JSearchPanel. * @param source the source set of tuples that should be searched over * @param search the search tuple set conducting the searches * @param fields the data fields being searched * @param monitorKeystrokes indicates if each keystroke event should result * in a new search being issued (true) or if searches should only be * initiated by hitting the enter key (false) */ public JSearchPanel(TupleSet source, SearchTupleSet search, String[] fields, boolean autoIndex, boolean monitorKeystrokes) { m_lock = new Object(); m_fields = fields; m_autoIndex = autoIndex; m_monitorKeys = monitorKeystrokes; m_searcher = ( search != null ? search : new PrefixSearchTupleSet() ); init(source); } // ------------------------------------------------------------------------ // Visualization-based constructors /** * Create a new JSearchPanel. The default search tuple set for the * visualization will be used. * @param vis the Visualization to search over * @param field the data field being searched */ public JSearchPanel(Visualization vis, String field) { this(vis, Visualization.ALL_ITEMS, field, true); } /** * Create a new JSearchPanel. The default search tuple set for the * visualization will be used. * @param vis the Visualization to search over * @param group the particular data group to search over * @param field the data field being searched */ public JSearchPanel(Visualization vis, String group, String field) { this(vis, group, field, true); } /** * Create a new JSearchPanel. The default search tuple set for the * visualization will be used. * @param vis the Visualization to search over * @param group the particular data group to search over * @param field the data field being searched * @param autoIndex indicates if items should be automatically * indexed and unindexed as their membership in the source group * changes. */ public JSearchPanel(Visualization vis, String group, String field, boolean autoIndex) { this(vis, group, Visualization.SEARCH_ITEMS, new String[] {field}, autoIndex, false); } /** * Create a new JSearchPanel. The default search tuple set for the * visualization will be used. * @param vis the Visualization to search over * @param group the particular data group to search over * @param field the data field being searched * @param autoIndex indicates if items should be automatically * indexed and unindexed as their membership in the source group * changes. * @param monitorKeystrokes indicates if each keystroke event should result * in a new search being issued (true) or if searches should only be * initiated by hitting the enter key (false) */ public JSearchPanel(Visualization vis, String group, String field, boolean autoIndex, boolean monitorKeystrokes) { this(vis, group, Visualization.SEARCH_ITEMS, new String[] {field}, autoIndex, true); } /** * Create a new JSearchPanel. * @param vis the Visualization to search over * @param group the particular data group to search over * @param searchGroup the group name that resolves to the SearchTupleSet * to use * @param field the data field being searched * @param autoIndex indicates if items should be automatically * indexed and unindexed as their membership in the source group * changes. * @param monitorKeystrokes indicates if each keystroke event should result * in a new search being issued (true) or if searches should only be * initiated by hitting the enter key (false) */ public JSearchPanel(Visualization vis, String group, String searchGroup, String field, boolean autoIndex, boolean monitorKeystrokes) { this(vis, group, searchGroup, new String[] {field}, autoIndex, monitorKeystrokes); } /** * Create a new JSearchPanel. * @param vis the Visualization to search over * @param group the particular data group to search over * @param searchGroup the group name that resolves to the SearchTupleSet * to use * @param fields the data fields being searched * @param autoIndex indicates if items should be automatically * indexed and unindexed as their membership in the source group * changes. * @param monitorKeystrokes indicates if each keystroke event should result * in a new search being issued (true) or if searches should only be * initiated by hitting the enter key (false) */ public JSearchPanel(Visualization vis, String group, String searchGroup, String[] fields, boolean autoIndex, boolean monitorKeystrokes) { m_lock = vis; m_fields = fields; m_autoIndex = autoIndex; m_monitorKeys = monitorKeystrokes; TupleSet search = vis.getGroup(searchGroup); if ( search != null ) { if ( search instanceof SearchTupleSet ) { m_searcher = (SearchTupleSet)search; } else { throw new IllegalStateException( "Search focus set not instance of SearchTupleSet!"); } } else { m_searcher = new PrefixSearchTupleSet(); vis.addFocusGroup(searchGroup, m_searcher); } init(vis.getGroup(group)); } // ------------------------------------------------------------------------ // Initialization private void init(TupleSet source) { if ( m_autoIndex && source != null ) { // index everything already there for ( int i=0; i < m_fields.length; i++ ) m_searcher.index(source.tuples(), m_fields[i]); // add a listener to dynamically build search index source.addTupleSetListener(new TupleSetListener() { public void tupleSetChanged(TupleSet tset, Tuple[] add, Tuple[] rem) { if ( add != null ) { for ( int i=0; i<add.length; ++i ) { for ( int j=0; j<m_fields.length; j++ ) m_searcher.index(add[i], m_fields[j]); } } if ( rem != null && m_searcher.isUnindexSupported() ) { for ( int i=0; i<rem.length; ++i ) { for ( int j=0; j<m_fields.length; j++ ) m_searcher.unindex(rem[i], m_fields[j]); } } } }); } m_queryF.addActionListener(this); if ( m_monitorKeys ) m_queryF.getDocument().addDocumentListener(this); m_queryF.setMaximumSize(new Dimension(400, 100)); m_queryF.setPreferredSize(new Dimension(200, 20)); m_queryF.setBorder(null); setBackground(Color.WHITE); initUI(); } private void initUI() { this.removeAll(); this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); m_sbox.removeAll(); m_sbox.add(Box.createHorizontalStrut(3)); m_sbox.add(m_queryF); m_sbox.add(Box.createHorizontalStrut(3)); if ( m_showCancel ) { m_sbox.add(new CancelButton()); m_sbox.add(Box.createHorizontalStrut(3)); } if ( m_showBorder ) m_sbox.setBorder(BorderFactory.createLineBorder(getForeground())); else m_sbox.setBorder(null); m_sbox.setMaximumSize(new Dimension(400, 100)); m_sbox.setPreferredSize(new Dimension(171, 20)); Box b = new Box(BoxLayout.X_AXIS); if ( m_includeHitCount ) { b.add(m_resultL); b.add(Box.createHorizontalStrut(10)); //b.add(Box.createHorizontalGlue()); } b.add(m_searchL); b.add(Box.createHorizontalStrut(3)); b.add(m_sbox); this.add(b); } // ------------------------------------------------------------------------ /** * Request the keyboard focus for this component. */ public void requestFocus() { this.m_queryF.requestFocus(); } /** * Set the lock, an object to synchronize on while issuing queries. * @param lock the synchronization lock */ public void setLock(Object lock) { m_lock = lock; } /** * Indicates if the component should show the number of search results. * @param b true to show the result count, false to hide it */ public void setShowResultCount(boolean b) { this.m_includeHitCount = b; initUI(); validate(); } /** * Indicates if the component should show a border around the text field. * @param b true to show the text field border, false to hide it */ public void setShowBorder(boolean b) { m_showBorder = b; initUI(); validate(); } /** * Indicates if the component should show the cancel query button. * @param b true to show the cancel query button, false to hide it */ public void setShowCancel(boolean b) { m_showCancel = b; initUI(); validate(); } /** * Update the search results based on the current query. */ protected void searchUpdate() { String query = m_queryF.getText(); synchronized ( m_lock ) { m_searcher.search(query); if ( m_searcher.getQuery().length() == 0 ) m_resultL.setText(null); else { int r = m_searcher.getTupleCount(); m_resultL.setText(r + " match" + (r==1?"":"es")); } } } /** * Set the query string in the text field. * @param query the query string to use */ public void setQuery(String query) { Document d = m_queryF.getDocument(); d.removeDocumentListener(this); m_queryF.setText(query); if ( m_monitorKeys ) d.addDocumentListener(this); searchUpdate(); } /** * Get the query string in the text field. * @return the current query string */ public String getQuery() { return m_queryF.getText(); } /** * Set the fill color of the cancel 'x' button that appears * when the button has the mouse pointer over it. * @param c the cancel color */ public void setCancelColor(Color c) { m_cancelColor = c; } /** * @see java.awt.Component#setBackground(java.awt.Color) */ public void setBackground(Color bg) { super.setBackground(bg); if ( m_queryF != null ) m_queryF.setBackground(bg); if ( m_resultL != null ) m_resultL.setBackground(bg); if ( m_searchL != null ) m_searchL.setBackground(bg); } /** * @see java.awt.Component#setForeground(java.awt.Color) */ public void setForeground(Color fg) { super.setForeground(fg); if ( m_queryF != null ) { m_queryF.setForeground(fg); m_queryF.setCaretColor(fg); } if ( m_resultL != null ) m_resultL.setForeground(fg); if ( m_searchL != null ) m_searchL.setForeground(fg); if ( m_sbox != null && m_showBorder ) m_sbox.setBorder(BorderFactory.createLineBorder(fg)); } /** * @see javax.swing.JComponent#setOpaque(boolean) */ public void setOpaque(boolean opaque) { super.setOpaque(opaque); if ( m_queryF != null ) { m_queryF.setOpaque(opaque); } if ( m_resultL != null ) m_resultL.setOpaque(opaque); if ( m_searchL != null ) m_searchL.setOpaque(opaque); } /** * @see java.awt.Component#setFont(java.awt.Font) */ public void setFont(Font f) { super.setFont(f);; if ( m_queryF != null ) m_queryF.setFont(f); if ( m_resultL != null ) m_resultL.setFont(f); if ( m_searchL != null ) m_searchL.setFont(f); } /** * Set the label text used on this component. * @param text the label text, use null to show no label */ public void setLabelText(String text) { m_searchL.setText(text); } /** * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent) */ public void changedUpdate(DocumentEvent e) { searchUpdate(); } /** * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent) */ public void insertUpdate(DocumentEvent e) { searchUpdate(); } /** * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent) */ public void removeUpdate(DocumentEvent e) { searchUpdate(); } /** * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) */ public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if ( src == m_queryF ) { searchUpdate(); } } /** * A button depicted as an "X" that allows users to cancel the current query * and clear the query field. */ public class CancelButton extends JComponent implements MouseListener { private boolean hover = false; private int[] outline = new int[] { 0,0, 2,0, 4,2, 5,2, 7,0, 9,0, 9,2, 7,4, 7,5, 9,7, 9,9, 7,9, 5,7, 4,7, 2,9, 0,9, 0,7, 2,5, 2,4, 0,2, 0,0 }; private int[] fill = new int[] { 1,1,8,8, 1,2,7,8, 2,1,8,7, 7,1,1,7, 8,2,2,8, 1,8,8,1 }; public CancelButton() { // set button size Dimension d = new Dimension(10,10); this.setPreferredSize(d); this.setMinimumSize(d); this.setMaximumSize(d); // prevent the widget from getting the keyboard focus this.setFocusable(false); // add callbacks this.addMouseListener(this); } public void paintComponent(Graphics g) { if ( hover ) { // draw fill g.setColor(m_cancelColor); for ( int i=0; i+3 < fill.length; i+=4 ) { g.drawLine(fill[i],fill[i+1],fill[i+2],fill[i+3]); } } g.setColor(JSearchPanel.this.getForeground()); for ( int i=0; i+3 < outline.length; i+=2 ) { g.drawLine(outline[i], outline[i+1], outline[i+2], outline[i+3]); } } public void mouseClicked(MouseEvent arg0) { setQuery(null); } public void mousePressed(MouseEvent arg0) { } public void mouseReleased(MouseEvent arg0) { } public void mouseEntered(MouseEvent arg0) { hover = true; repaint(); } public void mouseExited(MouseEvent arg0) { hover = false; repaint(); } } // end of class CancelButton } // end of class JSearchPanel