/* * Jajuk * Copyright (C) The Jajuk Team * http://jajuk.info * * 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 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ package org.jajuk.ui.widgets; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Collections; import java.util.List; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.DefaultListModel; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.ListCellRenderer; import javax.swing.Popup; import javax.swing.PopupFactory; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.jajuk.base.SearchResult; import org.jajuk.base.SearchResult.SearchResultType; import org.jajuk.base.TrackManager; import org.jajuk.services.players.QueueModel; import org.jajuk.services.players.StackItem; import org.jajuk.services.webradio.WebRadioHelper; import org.jajuk.ui.actions.JajukAction; import org.jajuk.ui.helpers.FontManager; import org.jajuk.ui.helpers.FontManager.JajukFont; import org.jajuk.util.Conf; import org.jajuk.util.Const; import org.jajuk.util.IconLoader; import org.jajuk.util.JajukIcons; import org.jajuk.util.Messages; import org.jajuk.util.UtilGUI; import org.jajuk.util.error.JajukException; import org.jajuk.util.log.Log; /** * Search combo box. Editable combo with search features. Comes with a default * selection implementation (see valueChanged() method) that could be changed */ public class SearchBox extends JTextField implements KeyListener, ListSelectionListener { /** Generated serialVersionUID. */ private static final long serialVersionUID = 1L; /** Do search panel need a search. */ private boolean bNeedSearch = false; /** Default time in ms before launching a search automatically. */ private static final int WAIT_TIME = 1000; /** Minimum number of characters to start a search. */ private static final int MIN_CRITERIA_LENGTH = 2; /** Search result. */ private List<SearchResult> alResults; /** Typed string. */ private String sTyped; private Popup popup; private JList jlist; private long lDateTyped; /** Search when typing timer. */ Timer timer = new Timer(100, new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { if (bNeedSearch && (System.currentTimeMillis() - lDateTyped >= WAIT_TIME)) { search(); } } }); /** * Display results as a jlabel with an icon. */ private static class SearchListRenderer extends JPanel implements ListCellRenderer { /** Generated serialVersionUID. */ private static final long serialVersionUID = 8975989658927794678L; /* * (non-Javadoc) * * @see javax.swing.ListCellRenderer#getListCellRendererComponent(javax.swing .JList, * java.lang.Object, int, boolean, boolean) */ @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { SearchResult sr = (SearchResult) value; JPanel jp = new JPanel(new BorderLayout()); JLabel jl = null; if (sr.getType() == SearchResultType.FILE) { jl = new JLabel(sr.getResu(), sr.getFile().getIconRepresentation(), SwingConstants.HORIZONTAL); } else if (sr.getType() == SearchResultType.WEBRADIO) { jl = new JLabel(sr.getResu(), IconLoader.getIcon(JajukIcons.WEBRADIO_16X16), SwingConstants.HORIZONTAL); } jp.add(jl, BorderLayout.WEST); return jp; } } /** * Constructor. */ public SearchBox() { setMargin(new Insets(0, 20, 0, 0)); addKeyListener(this); setToolTipText(Messages.getString("SearchBox.0")); // We use a font whose size cannot change with font size selected by user // because the search box cannot be enlarged vertically setFont(FontManager.getInstance().getFont(JajukFont.SEARCHBOX)); Color mediumGray = new Color(172, 172, 172); setForeground(mediumGray); installKeysrokes(); // Double click empties the field addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { super.mouseClicked(e); if (e.getClickCount() == 2) { setText(""); } } }); // Add a focus listener to select all the text and ease previous text cleanup addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { setCaretPosition(getText().length()); } @Override public void focusGained(FocusEvent e) { selectAll(); } }); } /* * (non-Javadoc) * * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent) */ @Override public void keyPressed(KeyEvent e) { // required by interface, but nothing to do here... } /* * (non-Javadoc) * * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent) */ @Override public void keyReleased(KeyEvent e) { if (e.getKeyChar() == KeyEvent.VK_ESCAPE && popup != null) { popup.hide(); return; } bNeedSearch = false; // stop clock for auto-search sTyped = getText(); if (sTyped.length() >= MIN_CRITERIA_LENGTH) { // perform automatic search only when user provide more than 5 // letters if (e.getKeyChar() == KeyEvent.VK_ENTER) { search(); } else { bNeedSearch = true; lDateTyped = System.currentTimeMillis(); // make sure the timer is started before it is first used if (!timer.isRunning()) { timer.start(); } } } else if (popup != null) { popup.hide(); } } /* * (non-Javadoc) * * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent) */ @Override public void keyTyped(KeyEvent e) { // required by interface, but nothing to do here... } /** * Perform a search when user stop to type in the search combo for 2 sec or * pressed enter. */ private void search() { bNeedSearch = false; setEnabled(false); // no typing during search // second test to get sure user didn't // typed before entering this method if (sTyped.length() >= MIN_CRITERIA_LENGTH) { SwingWorker<Void, Void> sw = new SwingWorker<Void, Void>() { List<SearchResult> resu = null; @Override public Void doInBackground() { try { UtilGUI.waiting(); resu = TrackManager.getInstance().search(sTyped); // Add web radio names resu.addAll(WebRadioHelper.search(sTyped)); // Sort the whole list Collections.sort(resu); } catch (Exception e) { Log.error(e); } return null; } @Override public void done() { if (resu != null && resu.size() > 0) { DefaultListModel model = new DefaultListModel(); SearchBox.this.alResults = resu; for (SearchResult sr : resu) { model.addElement(sr); } jlist = new JList(model); jlist.setLayoutOrientation(JList.VERTICAL); jlist.addListSelectionListener(SearchBox.this); jlist.setCellRenderer(new SearchListRenderer()); PopupFactory factory = PopupFactory.getSharedInstance(); JScrollPane jsp = new JScrollPane(jlist); int width = (int) ((float) Toolkit.getDefaultToolkit().getScreenSize().getWidth() * 0.9f); jsp.setMinimumSize(new Dimension(width, 250)); jsp.setPreferredSize(new Dimension(width, 250)); jsp.setMaximumSize(new Dimension(width, 250)); jlist.setSelectionMode(0); // For some reasons, we get the waiting cursor on the popup // sometimes, force it to default jlist.setCursor(UtilGUI.DEFAULT_CURSOR); jsp.setBorder(BorderFactory.createLineBorder(Color.BLACK)); if (popup != null) { popup.hide(); } // take upper-left point relative to the // textfield Point point = new Point(0, 0); // take absolute coordinates in the screen (popups works // only on absolute coordinates in opposition to swing // widgets) SwingUtilities.convertPointToScreen(point, SearchBox.this); int x = 10; int y = (int) point.getY() + 25; if ((int) point.getY() > 300) { y = (int) point.getY() - 250; } if (((int) point.getX() + 500 - width) > 0) { x = (int) point.getX() + 500 - width; } popup = factory.getPopup(null, jsp, x, y); popup.show(); jlist.addMouseListener(new MouseAdapter() { @Override public void mouseExited(MouseEvent e) { popup.hide(); } }); } else { if (popup != null) { popup.hide(); } } requestFocusInWindow(); setEnabled(true); UtilGUI.stopWaiting(); } }; sw.execute(); } } /** * Gets the selected index. * * @return the selected index */ public int getSelectedIndex() { return jlist.getSelectedIndex(); } /** * Gets the result or. * * @return the result */ public SearchResult getResult() { if (jlist == null) { return null; } return alResults.get(getSelectedIndex()); } /** * Hide popup. */ public void hidePopup() { popup.hide(); } /** * Display the search icon inside the texfield. * * @param g the graphics */ @Override public void paint(Graphics g) { super.paint(g); g.drawImage(IconLoader.getIcon(JajukIcons.SEARCH).getImage(), 4, 3, 16, 16, null); } /** * Default list selection implementation (may be overwritten for different * behavior). * * @param e */ @Override public void valueChanged(final ListSelectionEvent e) { SwingWorker<Void, Void> sw = new SwingWorker<Void, Void>() { @Override public Void doInBackground() { if (!e.getValueIsAdjusting()) { SearchResult sr = getResult(); try { // If user selected a file if (sr.getType() == SearchResultType.FILE) { QueueModel.push(new StackItem(sr.getFile(), Conf.getBoolean(Const.CONF_STATE_REPEAT), true), Conf.getBoolean(Const.CONF_OPTIONS_PUSH_ON_CLICK)); } // User selected a web radio else if (sr.getType() == SearchResultType.WEBRADIO && sr.getWebradio() != null) { QueueModel.launchRadio(sr.getWebradio()); } } catch (JajukException je) { Log.error(je); } } return null; } @Override public void done() { if (!e.getValueIsAdjusting()) { hidePopup(); requestFocusInWindow(); } } }; sw.execute(); } /** * Free up resources, timers, ... * * TODO: I could not find out any way to do this automatically! How can I * listen on some event that is sent when the enclosing dialog is closed? */ public void close() { // stop the timer so it does not keep the element from garbage collection timer.stop(); } /** * Search box specific keystrokes. */ private void installKeysrokes() { InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap = getActionMap(); inputMap.put(KeyStroke.getKeyStroke("ctrl F"), "search"); // We don't create a JajukAction dedicated class for this very simple case actionMap.put("search", new JajukAction("search", true) { private static final long serialVersionUID = 1L; @Override public void perform(ActionEvent evt) throws Exception { requestFocusInWindow(); } }); } }