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.BorderLayout;
022import java.awt.Dimension;
023import java.awt.Insets;
024import java.awt.Point;
025import java.awt.Rectangle;
026import java.awt.event.MouseAdapter;
027import java.awt.event.MouseEvent;
028import java.util.ArrayList;
029import java.util.List;
030
031import javax.swing.JScrollPane;
032import javax.swing.JTable;
033import javax.swing.JWindow;
034import javax.swing.ListSelectionModel;
035import javax.swing.Popup;
036import javax.swing.PopupFactory;
037import javax.swing.ScrollPaneConstants;
038import javax.swing.SwingUtilities;
039import javax.swing.event.ListSelectionEvent;
040import javax.swing.event.ListSelectionListener;
041import javax.swing.table.TableColumn;
042import javax.swing.table.TableColumnModel;
043
044import org.pushingpixels.flamingo.api.common.RichTooltip;
045import org.pushingpixels.flamingo.internal.ui.common.JRichTooltipPanel;
046
047import icy.main.Icy;
048import icy.search.SearchEngine;
049import icy.search.SearchResult;
050import icy.system.thread.ThreadUtil;
051
052/**
053 * This class is the most important part of this plugin: it will handle and
054 * display all local and online requests when characters are being typed in the {@link SearchBar}.
055 * 
056 * @author Thomas Provoost & Stephane
057 */
058public class SearchResultPanel extends JWindow implements ListSelectionListener
059{
060    /**
061     * 
062     */
063    private static final long serialVersionUID = -7794681892496197765L;
064
065    private static final int ROW_HEIGHT = 48;
066    private static final int MAX_ROW = 15;
067
068    /** Associated Search Bar */
069    final SearchBar searchBar;
070
071    /** PopupMenu */
072    JRichTooltipPanel tooltipPanel;
073    Popup tooltip;
074    SearchResult toolTipResult;
075    boolean toolTipForceRefresh;
076
077    /** GUI */
078    final SearchResultTableModel tableModel;
079    final JTable table;
080    final JScrollPane scrollPane;
081
082    /**
083     * Internals
084     */
085    private final Runnable refresher;
086    private final Runnable toolTipRefresher;
087
088    public SearchResultPanel(final SearchBar sb)
089    {
090        super(Icy.getMainInterface().getMainFrame());
091
092        searchBar = sb;
093
094        tooltipPanel = null;
095        tooltip = null;
096        toolTipResult = null;
097        toolTipForceRefresh = false;
098
099        refresher = new Runnable()
100        {
101            @Override
102            public void run()
103            {
104                refreshInternal();
105            }
106        };
107
108        toolTipRefresher = new Runnable()
109        {
110            @Override
111            public void run()
112            {
113                updateToolTip();
114            }
115        };
116
117        // build table (display 15 rows max)
118        tableModel = new SearchResultTableModel(-1);
119
120        table = new JTable(tableModel);
121
122        // sets the different column values and renderers
123        final TableColumnModel colModel = table.getColumnModel();
124        TableColumn col;
125
126        // provider name column
127        col = colModel.getColumn(0);
128        col.setCellRenderer(new SearchProducerTableCellRenderer());
129        col.setPreferredWidth(140);
130
131        // result text column
132        col = colModel.getColumn(1);
133        col.setCellRenderer(new SearchResultTableCellRenderer());
134        col.setPreferredWidth(600);
135
136        // sets the table properties
137        table.setIntercellSpacing(new Dimension(0, 0));
138        table.setShowVerticalLines(false);
139        table.setShowHorizontalLines(false);
140        table.setColumnSelectionAllowed(false);
141        table.setTableHeader(null);
142        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
143        table.getSelectionModel().addListSelectionListener(this);
144        table.addMouseListener(new MouseAdapter()
145        {
146            @Override
147            public void mouseClicked(MouseEvent e)
148            {
149                final SearchResult result = getResultAtPosition(e.getPoint());
150
151                if ((result != null) && result.isEnabled())
152                {
153                    if (SwingUtilities.isLeftMouseButton(e))
154                        result.execute();
155                    else
156                        result.executeAlternate();
157
158                    close(true);
159                    e.consume();
160                }
161            }
162
163            @Override
164            public void mouseExited(MouseEvent e)
165            {
166                // clear selection
167                table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
168            }
169
170            @Override
171            public void mouseEntered(MouseEvent e)
172            {
173                // select row under mouse position
174                final int row = table.rowAtPoint(e.getPoint());
175
176                if (row != -1)
177                    table.getSelectionModel().setSelectionInterval(row, row);
178                else
179                    table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
180            }
181        });
182        table.addMouseMotionListener(new MouseAdapter()
183        {
184            @Override
185            public void mouseMoved(MouseEvent e)
186            {
187                // select row under mouse position
188                final int row = table.rowAtPoint(e.getPoint());
189
190                if (row != -1)
191                    table.getSelectionModel().setSelectionInterval(row, row);
192                else
193                    table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
194            }
195        });
196
197        scrollPane = new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
198                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
199
200        // window used to display quick result list
201        setLayout(new BorderLayout());
202        add(scrollPane, BorderLayout.CENTER);
203        setPreferredSize(new Dimension(600, 400));
204        setAlwaysOnTop(true);
205        setVisible(false);
206    }
207
208    protected SearchEngine getSearchEngine()
209    {
210        return searchBar.getSearchEngine();
211    }
212
213    /**
214     * Returns SearchResult located at specified index.
215     */
216    protected SearchResult getResult(int index)
217    {
218        if ((index >= 0) && (index < table.getRowCount()))
219            return (SearchResult) table.getValueAt(index, SearchResultTableModel.COL_RESULT_OBJECT);
220
221        return null;
222    }
223
224    /**
225     * Returns the index in the table for the specified SearchResult (-1 if not found)
226     */
227    protected int getRowIndex(SearchResult result)
228    {
229        if (result != null)
230        {
231            for (int i = 0; i < table.getRowCount(); i++)
232                if (result == table.getValueAt(i, SearchResultTableModel.COL_RESULT_OBJECT))
233                    return i;
234        }
235
236        return -1;
237    }
238
239    /**
240     * Returns SearchResult located at specified point position.
241     */
242    protected SearchResult getResultAtPosition(Point pt)
243    {
244        return getResult(table.rowAtPoint(pt));
245    }
246
247    /**
248     * Returns selected result
249     */
250    public SearchResult getSelectedResult()
251    {
252        return getResult(table.getSelectedRow());
253    }
254
255    /**
256     * Set selected result
257     */
258    public void setSelectedResult(SearchResult result)
259    {
260        final int row = getRowIndex(result);
261
262        if (row != -1)
263            table.getSelectionModel().setSelectionInterval(row, row);
264        else
265            table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
266    }
267
268    void hideToolTip()
269    {
270        if (tooltip != null)
271        {
272            tooltip.hide();
273            tooltip = null;
274            toolTipResult = null;
275        }
276    }
277
278    /**
279     * Calculates and returns panel height.
280     */
281    int getPanelHeight()
282    {
283        final Insets margin = getInsets();
284        final Insets marginSC = scrollPane.getInsets();
285        final Insets marginT = table.getInsets();
286        int result;
287
288        result = Math.min(table.getRowCount(), MAX_ROW) * ROW_HEIGHT;
289        result += (margin.top + margin.bottom) + (marginSC.top + marginSC.bottom) + (marginT.top + marginT.bottom);
290
291        return result;
292    }
293
294    /**
295     * Updates the popup menu: asks the tablemodel for the right popupmenu and
296     * displays it.
297     */
298    void updateToolTip()
299    {
300        final SearchResult searchResult = getSelectedResult();
301
302        // need to be done on EDT
303        ThreadUtil.invokeNow(new Runnable()
304        {
305            @Override
306            public void run()
307            {
308                if (!isVisible() || (searchResult == null))
309                {
310                    hideToolTip();
311                    return;
312                }
313
314                final RichTooltip rtp = searchResult.getRichToolTip();
315
316                if (rtp == null)
317                {
318                    hideToolTip();
319                    return;
320                }
321
322                // tool tip is not yet visible or result changed --> refresh the tool tip
323                if ((tooltip == null) || (searchResult != toolTipResult) || toolTipForceRefresh)
324                {
325                    // hide out dated tool tip
326                    hideToolTip();
327
328                    final Rectangle bounds = getBounds();
329
330                    tooltipPanel = new JRichTooltipPanel(rtp);
331
332                    int x = bounds.x + bounds.width;
333                    int y = bounds.y + (ROW_HEIGHT * table.getSelectedRow());
334
335                    // adjust vertical position
336                    y -= scrollPane.getVerticalScrollBar().getValue();
337
338                    // show tooltip
339                    tooltip = PopupFactory.getSharedInstance().getPopup(Icy.getMainInterface().getMainFrame(),
340                            tooltipPanel, x, y);
341                    tooltip.show();
342
343                    toolTipResult = searchResult;
344                    toolTipForceRefresh = false;
345                }
346            }
347        });
348    }
349
350    /**
351     * Close the results panel.<br>
352     * If <code>reset</code> is true that also reset search.
353     */
354    public void close(boolean reset)
355    {
356        // reset search
357        if (reset)
358            searchBar.cancelSearch();
359
360        // hide popup and panel
361        setVisible(false);
362        hideToolTip();
363    }
364
365    /**
366     * Execute selected result.
367     * Return false if we don't have any selected result.
368     */
369    public void executeSelected()
370    {
371        final SearchResult sr = getSelectedResult();
372
373        if ((sr != null) && sr.isEnabled())
374        {
375            sr.execute();
376            close(true);
377        }
378    }
379
380    /**
381     * Update display
382     */
383    public void refresh()
384    {
385        ThreadUtil.runSingle(refresher);
386    }
387
388    /**
389     * Update display internal
390     */
391    void refreshInternal()
392    {
393        final SearchEngine searchEngine = getSearchEngine();
394        final List<SearchResult> results;
395        final int resultCount;
396        final SearchResult selected;
397
398        // quick cancel
399        if (searchEngine.getLastSearch().isEmpty())
400        {
401            results = new ArrayList<SearchResult>();
402            resultCount = 0;
403            selected = null;
404        }
405        else
406        {
407            results = searchEngine.getResults();
408            resultCount = results.size();
409            // save selected
410            selected = getSelectedResult();
411        }
412
413        // free a bit of time
414        ThreadUtil.sleep(1);
415
416        // need to be done on EDT
417        ThreadUtil.invokeNow(new Runnable()
418        {
419            @Override
420            public void run()
421            {
422                if (resultCount == 0)
423                {
424                    close(false);
425                    return;
426                }
427
428                // fix row height (can be changed on LAF change)
429                table.setRowHeight(ROW_HEIGHT);
430                // refresh data model
431                tableModel.setResults(results);
432                tableModel.fireTableDataChanged();
433
434                // restore selected
435                setSelectedResult(selected);
436
437                // update bounds and display window
438                final Point p = searchBar.getLocationOnScreen();
439                setBounds(p.x, p.y + searchBar.getHeight(), 600, getPanelHeight());
440
441                // show the result list
442                setVisible(true);
443
444                // update tooltip
445                updateToolTip();
446            }
447        });
448    }
449
450    /**
451     * Selection movement in the table: up or down.
452     * 
453     * @param direction
454     *        : should be 1 or -1.
455     */
456    public void moveSelection(int direction)
457    {
458        final int rowCount = table.getRowCount();
459
460        if (rowCount == 0)
461            return;
462
463        final int rowIndex = table.getSelectedRow();
464        final int newIndex;
465
466        if (rowIndex == -1)
467        {
468            if (direction > 0)
469                newIndex = 0;
470            else
471                newIndex = rowCount - 1;
472        }
473        else
474            newIndex = Math.abs((rowIndex + direction) % rowCount);
475
476        table.setRowSelectionInterval(newIndex, newIndex);
477    }
478
479    @Override
480    public void valueChanged(ListSelectionEvent e)
481    {
482        // selection changed --> update tooltip
483        ThreadUtil.runSingle(toolTipRefresher);
484    }
485
486    public void resultChanged(SearchResult result)
487    {
488        if (isVisible())
489        {
490            try
491            {
492                // only update the specified result
493                final int rowIndex = getRowIndex(result);
494
495                if (rowIndex != -1)
496                    tableModel.fireTableRowsUpdated(rowIndex, rowIndex);
497            }
498            catch (Exception e)
499            {
500                // ignore possible exception here
501            }
502
503            // refresh toolTip if needed
504            if (result == getSelectedResult())
505            {
506                toolTipForceRefresh = true;
507                ThreadUtil.runSingle(toolTipRefresher);
508            }
509        }
510    }
511
512    public void resultsChanged()
513    {
514        // refresh table
515        refresh();
516    }
517}