001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.menu.search;
020
021import java.awt.AWTEvent;
022import java.awt.AlphaComposite;
023import java.awt.Color;
024import java.awt.Graphics;
025import java.awt.Graphics2D;
026import java.awt.Insets;
027import java.awt.Point;
028import java.awt.Rectangle;
029import java.awt.RenderingHints;
030import java.awt.event.AWTEventListener;
031import java.awt.event.ActionEvent;
032import java.awt.event.FocusEvent;
033import java.awt.event.FocusListener;
034import java.awt.event.KeyEvent;
035import java.awt.event.MouseAdapter;
036import java.awt.event.MouseEvent;
037import java.awt.geom.Rectangle2D;
038import java.util.Timer;
039import java.util.TimerTask;
040
041import javax.swing.AbstractAction;
042import javax.swing.ActionMap;
043import javax.swing.InputMap;
044import javax.swing.JComponent;
045import javax.swing.KeyStroke;
046
047import org.jdesktop.swingx.painter.BusyPainter;
048
049import icy.gui.component.IcyTextField;
050import icy.resource.ResourceUtil;
051import icy.resource.icon.IcyIcon;
052import icy.search.SearchEngine;
053import icy.search.SearchEngine.SearchEngineListener;
054import icy.search.SearchResult;
055import icy.util.StringUtil;
056
057/**
058 * @author Thomas Provoost & Stephane.
059 */
060public class SearchBar extends IcyTextField implements SearchEngineListener
061{
062    /**
063     * 
064     */
065    private static final long serialVersionUID = -931313822004038942L;
066
067    private static final int DELAY = 20;
068
069    private static final int BUSY_PAINTER_SIZE = 15;
070    private static final int BUSY_PAINTER_POINTS = 40;
071    private static final int BUSY_PAINTER_TRAIL = 20;
072
073    /** Internal search engine */
074    final SearchEngine searchEngine;
075
076    /**
077     * GUI
078     */
079    final SearchResultPanel resultsPanel;
080    private final IcyIcon searchIcon;
081
082    /**
083     * Internals
084     */
085    private Timer busyPainterTimer;
086    final BusyPainter busyPainter;
087    int frame;
088    boolean lastSearchingState;
089    boolean initialized;
090
091    public SearchBar()
092    {
093        super();
094
095        initialized = false;
096
097        searchEngine = new SearchEngine();
098        searchEngine.addListener(this);
099
100        resultsPanel = new SearchResultPanel(this);
101        searchIcon = new IcyIcon(ResourceUtil.ICON_SEARCH, 16);
102
103        // modify margin so we have space for icon
104        final Insets margin = getMargin();
105        setMargin(new Insets(margin.top, margin.left, margin.bottom, margin.right + 20));
106
107        // focusable only when hit Ctrl + F or clicked at the beginning
108        setFocusable(false);
109
110        // SET THE BUSY PAINTER
111        busyPainter = new BusyPainter(BUSY_PAINTER_SIZE);
112        busyPainter.setFrame(0);
113        busyPainter.setPoints(BUSY_PAINTER_POINTS);
114        busyPainter.setTrailLength(BUSY_PAINTER_TRAIL);
115        busyPainter.setPointShape(new Rectangle2D.Float(0, 0, 2, 1));
116        frame = 0;
117
118        lastSearchingState = false;
119        busyPainterTimer = new Timer("Search animation timer");
120
121        // ADD LISTENERS
122        addTextChangeListener(new TextChangeListener()
123        {
124            @Override
125            public void textChanged(IcyTextField source, boolean validate)
126            {
127                searchInternal(getText());
128            }
129        });
130        addMouseListener(new MouseAdapter()
131        {
132            @Override
133            public void mouseClicked(MouseEvent e)
134            {
135                setFocus();
136            }
137        });
138
139        addFocusListener(new FocusListener()
140        {
141            @Override
142            public void focusLost(FocusEvent e)
143            {
144                removeFocus();
145            }
146
147            @Override
148            public void focusGained(FocusEvent e)
149            {
150                searchInternal(getText());
151            }
152        });
153
154        // global key listener to catch Ctrl+F in every case (not elegant)
155        // getToolkit().addAWTEventListener(new AWTEventListener()
156        // {
157        // @Override
158        // public void eventDispatched(AWTEvent event)
159        // {
160        // if (event instanceof KeyEvent)
161        // {
162        // final KeyEvent key = (KeyEvent) event;
163        //
164        // if (key.getID() == KeyEvent.KEY_PRESSED)
165        // {
166        // // Handle key presses
167        // switch (key.getKeyCode())
168        // {
169        // case KeyEvent.VK_F:
170        // if (EventUtil.isControlDown(key))
171        // {
172        // setFocus();
173        // key.consume();
174        // }
175        // break;
176        // }
177        // }
178        // }
179        // }
180        // }, AWTEvent.KEY_EVENT_MASK);
181
182        // global mouse listener to simulate focus lost (not elegant)
183        getToolkit().addAWTEventListener(new AWTEventListener()
184        {
185            @Override
186            public void eventDispatched(AWTEvent event)
187            {
188                if (!initialized || !hasFocus())
189                    return;
190
191                if (event instanceof MouseEvent)
192                {
193                    final MouseEvent evt = (MouseEvent) event;
194
195                    if (evt.getID() == MouseEvent.MOUSE_PRESSED)
196                    {
197                        final Point pt = evt.getLocationOnScreen();
198
199                        // user clicked outside search panel --> close it
200                        if (!isInsideSearchComponents(pt))
201                            removeFocus();
202                    }
203                }
204            }
205        }, AWTEvent.MOUSE_EVENT_MASK);
206
207        buildActionMap();
208
209        initialized = true;
210    }
211
212    void buildActionMap()
213    {
214        final InputMap imap = getInputMap(JComponent.WHEN_FOCUSED);
215        final ActionMap amap = getActionMap();
216
217        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel");
218        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "MoveDown");
219        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "MoveUp");
220        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Execute");
221
222        amap.put("Cancel", new AbstractAction()
223        {
224            /**
225             * 
226             */
227            private static final long serialVersionUID = 6690317671269902666L;
228
229            @Override
230            public void actionPerformed(ActionEvent e)
231            {
232                if (initialized)
233                    cancelSearch();
234            }
235        });
236        getActionMap().put("MoveDown", new AbstractAction()
237        {
238            /**
239             * 
240             */
241            private static final long serialVersionUID = 8864361043092897904L;
242
243            @Override
244            public void actionPerformed(ActionEvent e)
245            {
246                if (initialized)
247                    moveDown();
248            }
249        });
250        getActionMap().put("MoveUp", new AbstractAction()
251        {
252            /**
253             * 
254             */
255            private static final long serialVersionUID = 6258168037713535447L;
256
257            @Override
258            public void actionPerformed(ActionEvent e)
259            {
260                if (initialized)
261                    moveUp();
262            }
263        });
264        getActionMap().put("Execute", new AbstractAction()
265        {
266            /**
267             * 
268             */
269            private static final long serialVersionUID = 5363650211730888168L;
270
271            @Override
272            public void actionPerformed(ActionEvent e)
273            {
274                if (initialized)
275                    execute();
276            }
277        });
278    }
279
280    public SearchEngine getSearchEngine()
281    {
282        return searchEngine;
283    }
284
285    protected boolean isInsideSearchComponents(Point pt)
286    {
287        final Rectangle bounds = new Rectangle();
288
289        bounds.setLocation(getLocationOnScreen());
290        bounds.setSize(getSize());
291
292        if (bounds.contains(pt))
293            return true;
294
295        if (initialized)
296        {
297            if (resultsPanel.isVisible())
298            {
299                bounds.setLocation(resultsPanel.getLocationOnScreen());
300                bounds.setSize(resultsPanel.getSize());
301
302                return bounds.contains(pt);
303            }
304        }
305
306        return false;
307    }
308
309    public void setFocus()
310    {
311        if (!hasFocus())
312        {
313            setFocusable(true);
314            requestFocus();
315        }
316    }
317
318    public void removeFocus()
319    {
320        if (initialized)
321        {
322            resultsPanel.close(true);
323            setFocusable(false);
324        }
325    }
326
327    public void cancelSearch()
328    {
329        setText("");
330    }
331
332    // public void search(String text)
333    // {
334    // final String filter = text.trim();
335    //
336    // if (StringUtil.isEmpty(filter))
337    // searchEngine.cancelSearch();
338    // else
339    // searchEngine.search(filter);
340    // }
341    //
342    /**
343     * Request search for the specified text.
344     * 
345     * @see SearchEngine#search(String)
346     */
347    public void search(String text)
348    {
349        setText(text);
350    }
351
352    protected void searchInternal(String text)
353    {
354        final String filter = text.trim();
355
356        if (StringUtil.isEmpty(filter))
357            searchEngine.cancelSearch();
358        else
359            searchEngine.search(filter);
360    }
361
362    protected void execute()
363    {
364        // result displayed --> launch selected result
365        if (resultsPanel.isShowing())
366            resultsPanel.executeSelected();
367        else
368            searchInternal(getText());
369    }
370
371    protected void moveDown()
372    {
373        resultsPanel.moveSelection(1);
374    }
375
376    protected void moveUp()
377    {
378        resultsPanel.moveSelection(-1);
379    }
380
381    @Override
382    protected void paintComponent(Graphics g)
383    {
384        super.paintComponent(g);
385
386        Graphics2D g2 = (Graphics2D) g.create();
387        int w = getWidth();
388        int h = getHeight();
389
390        // set rendering presets
391        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
392        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
393
394        if (StringUtil.isEmpty(getText()) && !hasFocus())
395        {
396            // draw "Search" if no focus
397            Insets insets = getMargin();
398            Color fg = getForeground();
399
400            g2.setColor(new Color(fg.getRed(), fg.getGreen(), fg.getBlue(), 100));
401            g2.drawString("Search", insets.left + 2, h - g2.getFontMetrics().getHeight() / 2 + 2);
402        }
403
404        if (searchEngine.isSearching())
405        {
406            // draw loading icon
407            g2.translate(w - (BUSY_PAINTER_SIZE + 5), 3);
408            busyPainter.paint(g2, this, BUSY_PAINTER_SIZE, BUSY_PAINTER_SIZE);
409        }
410        else
411        {
412            // draw search icon
413            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
414            searchIcon.paintIcon(this, g2, w - h, 2);
415        }
416
417        g2.dispose();
418    }
419
420    @Override
421    public void resultChanged(SearchEngine source, SearchResult result)
422    {
423        if (initialized)
424            resultsPanel.resultChanged(result);
425    }
426
427    @Override
428    public void resultsChanged(SearchEngine source)
429    {
430        if (initialized)
431            resultsPanel.resultsChanged();
432    }
433
434    @Override
435    public void searchStarted(SearchEngine source)
436    {
437        if (!initialized)
438            return;
439
440        // make sure the animation timer for the busy icon is stopped
441        busyPainterTimer.cancel();
442
443        // ... and restart it
444        final Timer newTimer = new Timer("Search animation timer");
445        newTimer.scheduleAtFixedRate(new TimerTask()
446        {
447            @Override
448            public void run()
449            {
450                frame = (frame + 1) % BUSY_PAINTER_POINTS;
451                busyPainter.setFrame(frame);
452
453                final boolean searching = searchEngine.isSearching();
454
455                // this permit to get rid of the small delay between the searchCompleted
456                // event and when isSearching() actually returns false
457                if (searching || (searching != lastSearchingState))
458                    repaint();
459
460                lastSearchingState = searching;
461            }
462        }, DELAY, DELAY);
463        busyPainterTimer = newTimer;
464
465        // for the busy loop animation
466        repaint();
467    }
468
469    @Override
470    public void searchCompleted(SearchEngine source)
471    {
472        // stop the animation timer for the rotating busy icon
473        busyPainterTimer.cancel();
474
475        // for the busy loop animation
476        repaint();
477    }
478}