/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy 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 3 of the License, or * (at your option) any later version. * * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>. */ package icy.gui.menu.search; import icy.gui.component.IcyTextField; import icy.resource.ResourceUtil; import icy.resource.icon.IcyIcon; import icy.search.SearchEngine; import icy.search.SearchEngine.SearchEngineListener; import icy.search.SearchResult; import icy.util.StringUtil; import java.awt.AWTEvent; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.util.Timer; import java.util.TimerTask; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.KeyStroke; import org.jdesktop.swingx.painter.BusyPainter; /** * @author Thomas Provoost & Stephane. */ public class SearchBar extends IcyTextField implements SearchEngineListener { /** * */ private static final long serialVersionUID = -931313822004038942L; private static final int DELAY = 20; private static final int BUSY_PAINTER_SIZE = 15; private static final int BUSY_PAINTER_POINTS = 40; private static final int BUSY_PAINTER_TRAIL = 20; /** Internal search engine */ final SearchEngine searchEngine; /** * GUI */ final SearchResultPanel resultsPanel; private final IcyIcon searchIcon; /** * Internals */ private Timer busyPainterTimer; final BusyPainter busyPainter; int frame; boolean lastSearchingState; boolean initialized; public SearchBar() { super(); initialized = false; searchEngine = new SearchEngine(); searchEngine.addListener(this); resultsPanel = new SearchResultPanel(this); searchIcon = new IcyIcon(ResourceUtil.ICON_SEARCH, 16); // modify margin so we have space for icon final Insets margin = getMargin(); setMargin(new Insets(margin.top, margin.left, margin.bottom, margin.right + 20)); // focusable only when hit Ctrl + F or clicked at the beginning setFocusable(false); // SET THE BUSY PAINTER busyPainter = new BusyPainter(BUSY_PAINTER_SIZE); busyPainter.setFrame(0); busyPainter.setPoints(BUSY_PAINTER_POINTS); busyPainter.setTrailLength(BUSY_PAINTER_TRAIL); busyPainter.setPointShape(new Rectangle2D.Float(0, 0, 2, 1)); frame = 0; lastSearchingState = false; busyPainterTimer = new Timer("Search animation timer"); // ADD LISTENERS addTextChangeListener(new TextChangeListener() { @Override public void textChanged(IcyTextField source, boolean validate) { searchInternal(getText()); } }); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { setFocus(); } }); addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { removeFocus(); } @Override public void focusGained(FocusEvent e) { searchInternal(getText()); } }); // global key listener to catch Ctrl+F in every case (not elegant) // getToolkit().addAWTEventListener(new AWTEventListener() // { // @Override // public void eventDispatched(AWTEvent event) // { // if (event instanceof KeyEvent) // { // final KeyEvent key = (KeyEvent) event; // // if (key.getID() == KeyEvent.KEY_PRESSED) // { // // Handle key presses // switch (key.getKeyCode()) // { // case KeyEvent.VK_F: // if (EventUtil.isControlDown(key)) // { // setFocus(); // key.consume(); // } // break; // } // } // } // } // }, AWTEvent.KEY_EVENT_MASK); // global mouse listener to simulate focus lost (not elegant) getToolkit().addAWTEventListener(new AWTEventListener() { @Override public void eventDispatched(AWTEvent event) { if (!initialized || !hasFocus()) return; if (event instanceof MouseEvent) { final MouseEvent evt = (MouseEvent) event; if (evt.getID() == MouseEvent.MOUSE_PRESSED) { final Point pt = evt.getLocationOnScreen(); // user clicked outside search panel --> close it if (!isInsideSearchComponents(pt)) removeFocus(); } } } }, AWTEvent.MOUSE_EVENT_MASK); buildActionMap(); initialized = true; } void buildActionMap() { final InputMap imap = getInputMap(JComponent.WHEN_FOCUSED); final ActionMap amap = getActionMap(); imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel"); imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "MoveDown"); imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "MoveUp"); imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Execute"); amap.put("Cancel", new AbstractAction() { /** * */ private static final long serialVersionUID = 6690317671269902666L; @Override public void actionPerformed(ActionEvent e) { if (initialized) cancelSearch(); } }); getActionMap().put("MoveDown", new AbstractAction() { /** * */ private static final long serialVersionUID = 8864361043092897904L; @Override public void actionPerformed(ActionEvent e) { if (initialized) moveDown(); } }); getActionMap().put("MoveUp", new AbstractAction() { /** * */ private static final long serialVersionUID = 6258168037713535447L; @Override public void actionPerformed(ActionEvent e) { if (initialized) moveUp(); } }); getActionMap().put("Execute", new AbstractAction() { /** * */ private static final long serialVersionUID = 5363650211730888168L; @Override public void actionPerformed(ActionEvent e) { if (initialized) execute(); } }); } public SearchEngine getSearchEngine() { return searchEngine; } protected boolean isInsideSearchComponents(Point pt) { final Rectangle bounds = new Rectangle(); bounds.setLocation(getLocationOnScreen()); bounds.setSize(getSize()); if (bounds.contains(pt)) return true; if (initialized) { if (resultsPanel.isVisible()) { bounds.setLocation(resultsPanel.getLocationOnScreen()); bounds.setSize(resultsPanel.getSize()); return bounds.contains(pt); } } return false; } public void setFocus() { if (!hasFocus()) { setFocusable(true); requestFocus(); } } public void removeFocus() { if (initialized) { resultsPanel.close(true); setFocusable(false); } } public void cancelSearch() { setText(""); } // public void search(String text) // { // final String filter = text.trim(); // // if (StringUtil.isEmpty(filter)) // searchEngine.cancelSearch(); // else // searchEngine.search(filter); // } // /** * Request search for the specified text. * * @see SearchEngine#search(String) */ public void search(String text) { setText(text); } protected void searchInternal(String text) { final String filter = text.trim(); if (StringUtil.isEmpty(filter)) searchEngine.cancelSearch(); else searchEngine.search(filter); } protected void execute() { // result displayed --> launch selected result if (resultsPanel.isShowing()) resultsPanel.executeSelected(); else searchInternal(getText()); } protected void moveDown() { resultsPanel.moveSelection(1); } protected void moveUp() { resultsPanel.moveSelection(-1); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g.create(); int w = getWidth(); int h = getHeight(); // set rendering presets g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); if (StringUtil.isEmpty(getText()) && !hasFocus()) { // draw "Search" if no focus Insets insets = getMargin(); Color fg = getForeground(); g2.setColor(new Color(fg.getRed(), fg.getGreen(), fg.getBlue(), 100)); g2.drawString("Search", insets.left + 2, h - g2.getFontMetrics().getHeight() / 2 + 2); } if (searchEngine.isSearching()) { // draw loading icon g2.translate(w - (BUSY_PAINTER_SIZE + 5), 3); busyPainter.paint(g2, this, BUSY_PAINTER_SIZE, BUSY_PAINTER_SIZE); } else { // draw search icon g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f)); searchIcon.paintIcon(this, g2, w - h, 2); } g2.dispose(); } @Override public void resultChanged(SearchEngine source, SearchResult result) { if (initialized) resultsPanel.resultChanged(result); } @Override public void resultsChanged(SearchEngine source) { if (initialized) resultsPanel.resultsChanged(); } @Override public void searchStarted(SearchEngine source) { if (!initialized) return; resultsPanel.searchStarted(); // make sure the animation timer for the busy icon is stopped busyPainterTimer.cancel(); // ... and restart it final Timer newTimer = new Timer("Search animation timer"); newTimer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { frame = (frame + 1) % BUSY_PAINTER_POINTS; busyPainter.setFrame(frame); final boolean searching = searchEngine.isSearching(); // this permit to get rid of the small delay between the searchCompleted // event and when isSearching() actually returns false if (searching || (searching != lastSearchingState)) repaint(); lastSearchingState = searching; } }, DELAY, DELAY); busyPainterTimer = newTimer; // for the busy loop animation repaint(); } @Override public void searchCompleted(SearchEngine source) { // stop the animation timer for the rotating busy icon busyPainterTimer.cancel(); // for the busy loop animation repaint(); } }