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.preferences;
020
021import icy.gui.component.IcyTable;
022import icy.gui.component.IcyTextField;
023import icy.gui.component.IcyTextField.TextChangeListener;
024import icy.gui.plugin.PluginDetailPanel;
025import icy.gui.util.ComponentUtil;
026import icy.network.NetworkUtil;
027import icy.plugin.PluginDescriptor;
028import icy.preferences.RepositoryPreferences;
029import icy.preferences.RepositoryPreferences.RepositoryInfo;
030import icy.resource.ResourceUtil;
031import icy.system.thread.ThreadUtil;
032import icy.util.StringUtil;
033
034import java.awt.BorderLayout;
035import java.awt.Dimension;
036import java.awt.event.ActionEvent;
037import java.awt.event.ActionListener;
038import java.awt.event.MouseAdapter;
039import java.awt.event.MouseEvent;
040import java.util.ArrayList;
041import java.util.HashSet;
042import java.util.List;
043
044import javax.swing.BorderFactory;
045import javax.swing.Box;
046import javax.swing.BoxLayout;
047import javax.swing.ImageIcon;
048import javax.swing.JButton;
049import javax.swing.JComboBox;
050import javax.swing.JLabel;
051import javax.swing.JPanel;
052import javax.swing.JScrollPane;
053import javax.swing.JTable;
054import javax.swing.ListSelectionModel;
055import javax.swing.event.ListSelectionEvent;
056import javax.swing.event.ListSelectionListener;
057import javax.swing.table.AbstractTableModel;
058import javax.swing.table.TableColumn;
059import javax.swing.table.TableColumnModel;
060
061/**
062 * @author Stephane
063 */
064public abstract class PluginListPreferencePanel extends PreferencePanel
065        implements TextChangeListener, ListSelectionListener
066{
067    /**
068     * 
069     */
070    private static final long serialVersionUID = -2718763355377652489L;
071
072    static final String[] columnNames = {"", "Name", "Version", "State", "Enabled"};
073    static final String[] columnIds = {"Icon", "Name", "Version", "State", "Enabled"};
074
075    List<PluginDescriptor> plugins;
076
077    /**
078     * gui
079     */
080    final AbstractTableModel tableModel;
081    final JTable table;
082
083    final JComboBox repository;
084    final JPanel repositoryPanel;
085    final IcyTextField filter;
086    final JButton refreshButton;
087    final JButton documentationButton;
088    final JButton detailButton;
089    final JButton action1Button;
090    final JButton action2Button;
091
092    private final Runnable buttonsStateUpdater;
093    private final Runnable tableDataRefresher;
094    private final Runnable pluginsListRefresher;
095    private final Runnable repositoriesUpdater;
096
097    final ActionListener repositoryActionListener;
098
099    PluginListPreferencePanel(PreferenceFrame parent, String nodeName, String parentName)
100    {
101        super(parent, nodeName, parentName);
102
103        plugins = new ArrayList<PluginDescriptor>();
104
105        buttonsStateUpdater = new Runnable()
106        {
107            @Override
108            public void run()
109            {
110                // need to be done on EDT
111                ThreadUtil.invokeNow(new Runnable()
112                {
113                    @Override
114                    public void run()
115                    {
116                        updateButtonsStateInternal();
117                    }
118                });
119            }
120        };
121        tableDataRefresher = new Runnable()
122        {
123            @Override
124            public void run()
125            {
126                // need to be done on EDT
127                ThreadUtil.invokeNow(new Runnable()
128                {
129                    @Override
130                    public void run()
131                    {
132                        refreshTableDataInternal();
133                    }
134                });
135            }
136        };
137        pluginsListRefresher = new Runnable()
138        {
139            @Override
140            public void run()
141            {
142                refreshPluginsInternal();
143            }
144        };
145        repositoriesUpdater = new Runnable()
146        {
147            @Override
148            public void run()
149            {
150                // need to be done on EDT
151                ThreadUtil.invokeNow(new Runnable()
152                {
153                    @Override
154                    public void run()
155                    {
156                        updateRepositoriesInternal();
157                    }
158                });
159            }
160        };
161        repositoryActionListener = new ActionListener()
162        {
163            @Override
164            public void actionPerformed(ActionEvent e)
165            {
166                repositoryChanged();
167            }
168        };
169
170        repository = new JComboBox();
171        repository.setToolTipText("Select a repository");
172        repository.addActionListener(repositoryActionListener);
173
174        repositoryPanel = new JPanel();
175        repositoryPanel.setLayout(new BoxLayout(repositoryPanel, BoxLayout.PAGE_AXIS));
176        repositoryPanel.setVisible(false);
177
178        final JPanel internalRepPanel = new JPanel();
179        internalRepPanel.setLayout(new BoxLayout(internalRepPanel, BoxLayout.LINE_AXIS));
180
181        internalRepPanel.add(new JLabel("Repository :"));
182        internalRepPanel.add(Box.createHorizontalStrut(8));
183        internalRepPanel.add(repository);
184        internalRepPanel.add(Box.createHorizontalGlue());
185
186        repositoryPanel.add(internalRepPanel);
187        repositoryPanel.add(Box.createVerticalStrut(8));
188
189        // need filter before load()
190        filter = new IcyTextField();
191        filter.addTextChangeListener(this);
192
193        // build buttons panel
194        final Dimension buttonsDim = new Dimension(100, 24);
195
196        refreshButton = new JButton("Reload list");
197        refreshButton.addActionListener(new ActionListener()
198        {
199            @Override
200            public void actionPerformed(ActionEvent e)
201            {
202                reloadPlugins();
203            }
204        });
205        ComponentUtil.setFixedSize(refreshButton, buttonsDim);
206
207        documentationButton = new JButton("Online doc");
208        documentationButton.setToolTipText("Open the online documentation");
209        documentationButton.addActionListener(new ActionListener()
210        {
211            @Override
212            public void actionPerformed(ActionEvent e)
213            {
214                final List<PluginDescriptor> selectedPlugins = getSelectedPlugins();
215
216                // open plugin web page
217                if (selectedPlugins.size() == 1)
218                    NetworkUtil.openBrowser(selectedPlugins.get(0).getWeb());
219            }
220        });
221        ComponentUtil.setFixedSize(documentationButton, buttonsDim);
222
223        detailButton = new JButton("Show detail");
224        detailButton.addActionListener(new ActionListener()
225        {
226            @Override
227            public void actionPerformed(ActionEvent e)
228            {
229                final List<PluginDescriptor> selectedPlugins = getSelectedPlugins();
230
231                // open the detail
232                if (selectedPlugins.size() == 1)
233                    new PluginDetailPanel(selectedPlugins.get(0));
234            }
235        });
236        ComponentUtil.setFixedSize(detailButton, buttonsDim);
237
238        action1Button = new JButton("null");
239        action1Button.addActionListener(new ActionListener()
240        {
241            @Override
242            public void actionPerformed(ActionEvent e)
243            {
244                doAction1();
245            }
246        });
247        action1Button.setVisible(false);
248        ComponentUtil.setFixedSize(action1Button, buttonsDim);
249
250        action2Button = new JButton("null");
251        action2Button.addActionListener(new ActionListener()
252        {
253            @Override
254            public void actionPerformed(ActionEvent e)
255            {
256                doAction2();
257            }
258        });
259        action2Button.setVisible(false);
260        ComponentUtil.setFixedSize(action2Button, buttonsDim);
261
262        final JPanel buttonsPanel = new JPanel();
263        buttonsPanel.setBorder(BorderFactory.createEmptyBorder(4, 8, 8, 8));
264        buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.PAGE_AXIS));
265
266        buttonsPanel.add(refreshButton);
267        buttonsPanel.add(Box.createVerticalStrut(34));
268        buttonsPanel.add(documentationButton);
269        buttonsPanel.add(Box.createVerticalStrut(8));
270        buttonsPanel.add(detailButton);
271        buttonsPanel.add(Box.createVerticalStrut(8));
272        buttonsPanel.add(action1Button);
273        buttonsPanel.add(Box.createVerticalStrut(8));
274        buttonsPanel.add(action2Button);
275        buttonsPanel.add(Box.createVerticalStrut(8));
276        buttonsPanel.add(Box.createVerticalGlue());
277
278        // build table
279        tableModel = new AbstractTableModel()
280        {
281            /**
282             * 
283             */
284            private static final long serialVersionUID = -8573364273165723214L;
285
286            @Override
287            public int getColumnCount()
288            {
289                return columnNames.length;
290            }
291
292            @Override
293            public String getColumnName(int column)
294            {
295                return columnNames[column];
296            }
297
298            @Override
299            public int getRowCount()
300            {
301                return plugins.size();
302            }
303
304            @Override
305            public Object getValueAt(int row, int column)
306            {
307                if (row < plugins.size())
308                {
309                    final PluginDescriptor plugin = plugins.get(row);
310
311                    switch (column)
312                    {
313                        case 0:
314                            if (plugin.isIconLoaded())
315                                return ResourceUtil.scaleIcon(plugin.getIcon(), 32);
316
317                            loadIconAsync(plugin);
318                            return ResourceUtil.scaleIcon(PluginDescriptor.DEFAULT_ICON, 32);
319
320                        case 1:
321                            return plugin.getName();
322
323                        case 2:
324                            return plugin.getVersion().toString();
325
326                        case 3:
327                            return getStateValue(plugin);
328
329                        case 4:
330                            return Boolean.valueOf(isActive(plugin));
331                    }
332                }
333
334                return "";
335            }
336
337            @Override
338            public void setValueAt(Object aValue, int rowIndex, int columnIndex)
339            {
340                if (rowIndex < plugins.size())
341                {
342                    final PluginDescriptor plugin = plugins.get(rowIndex);
343
344                    if (columnIndex == 4)
345                    {
346                        if (aValue instanceof Boolean)
347                            setActive(plugin, ((Boolean) aValue).booleanValue());
348                    }
349                }
350            }
351
352            @Override
353            public boolean isCellEditable(int row, int column)
354            {
355                return (column == 4);
356            }
357
358            @Override
359            public Class<?> getColumnClass(int columnIndex)
360            {
361                switch (columnIndex)
362                {
363                    case 0:
364                        return ImageIcon.class;
365                    case 4:
366                        return Boolean.class;
367                    default:
368                        return String.class;
369                }
370            }
371        };
372
373        table = new IcyTable(tableModel);
374
375        final TableColumnModel colModel = table.getColumnModel();
376        TableColumn col;
377
378        // columns setting
379        col = colModel.getColumn(0);
380        col.setIdentifier(columnIds[0]);
381        col.setMinWidth(32);
382        col.setPreferredWidth(32);
383        col.setMaxWidth(32);
384
385        col = colModel.getColumn(1);
386        col.setIdentifier(columnIds[1]);
387        col.setMinWidth(120);
388        col.setPreferredWidth(200);
389        col.setMaxWidth(500);
390
391        col = colModel.getColumn(2);
392        col.setIdentifier(columnIds[2]);
393        col.setMinWidth(60);
394        col.setPreferredWidth(60);
395        col.setMaxWidth(60);
396
397        col = colModel.getColumn(3);
398        col.setIdentifier(columnIds[3]);
399        col.setMinWidth(70);
400        col.setPreferredWidth(90);
401        col.setMaxWidth(120);
402
403        col = colModel.getColumn(4);
404        col.setIdentifier(columnIds[4]);
405        col.setMinWidth(60);
406        col.setPreferredWidth(60);
407        col.setMaxWidth(60);
408
409        table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
410        table.getSelectionModel().addListSelectionListener(this);
411        table.setRowHeight(32);
412        table.setColumnSelectionAllowed(false);
413        table.setRowSelectionAllowed(true);
414        table.setShowVerticalLines(false);
415        table.setAutoCreateRowSorter(true);
416        // sort on name by default
417        table.getRowSorter().toggleSortOrder(1);
418        table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
419        table.addMouseListener(new MouseAdapter()
420        {
421            @Override
422            public void mouseClicked(MouseEvent me)
423            {
424                if (!me.isConsumed())
425                {
426                    if (me.getClickCount() == 2)
427                    {
428                        // show detail
429                        detailButton.doClick();
430                        me.consume();
431                    }
432                }
433            }
434        });
435
436        final JPanel tableTopPanel = new JPanel();
437
438        tableTopPanel.setLayout(new BoxLayout(tableTopPanel, BoxLayout.PAGE_AXIS));
439
440        tableTopPanel.add(Box.createVerticalStrut(2));
441        tableTopPanel.add(repositoryPanel);
442        tableTopPanel.add(filter);
443        tableTopPanel.add(Box.createVerticalStrut(8));
444        tableTopPanel.add(table.getTableHeader());
445
446        final JPanel tablePanel = new JPanel();
447
448        tablePanel.setLayout(new BorderLayout());
449
450        tablePanel.add(tableTopPanel, BorderLayout.NORTH);
451        tablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
452
453        mainPanel.setLayout(new BorderLayout());
454
455        mainPanel.add(tablePanel, BorderLayout.CENTER);
456        mainPanel.add(buttonsPanel, BorderLayout.EAST);
457
458        mainPanel.validate();
459    }
460
461    protected void loadIconAsync(final PluginDescriptor plugin)
462    {
463        ThreadUtil.bgRun(new Runnable()
464        {
465            @Override
466            public void run()
467            {
468                // icon correctly loaded ?
469                if (plugin.loadIcon())
470                    refreshTableData();
471            }
472        });
473    }
474
475    @Override
476    protected void closed()
477    {
478        super.closed();
479
480        // do not retains plugins when frame is closed
481        plugins.clear();
482    }
483
484    private List<PluginDescriptor> filterList(List<PluginDescriptor> list, String filter)
485    {
486        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
487        final boolean empty = StringUtil.isEmpty(filter, true);
488        final String filterUp;
489
490        if (!empty)
491            filterUp = filter.toUpperCase();
492        else
493            filterUp = "";
494
495        for (PluginDescriptor plugin : list)
496        {
497            final String classname = plugin.getClassName().toUpperCase();
498            final String name = plugin.getName().toUpperCase();
499            final String desc = plugin.getDescription().toUpperCase();
500
501            // search in name and description
502            if (empty || (classname.indexOf(filterUp) != -1) || (name.indexOf(filterUp) != -1)
503                    || (desc.indexOf(filterUp) != -1))
504                result.add(plugin);
505        }
506
507        return result;
508    }
509
510    protected boolean isActive(PluginDescriptor plugin)
511    {
512        return false;
513    }
514
515    protected void setActive(PluginDescriptor plugin, boolean value)
516    {
517    }
518
519    protected abstract void doAction1();
520
521    protected abstract void doAction2();
522
523    protected abstract void repositoryChanged();
524
525    protected abstract void reloadPlugins();
526
527    protected abstract String getStateValue(PluginDescriptor plugin);
528
529    protected abstract List<PluginDescriptor> getPlugins();
530
531    protected int getPluginTableIndex(int rowIndex)
532    {
533        if (rowIndex == -1)
534            return rowIndex;
535
536        try
537        {
538
539            return table.convertRowIndexToView(rowIndex);
540        }
541        catch (IndexOutOfBoundsException e)
542        {
543            return -1;
544        }
545    }
546
547    protected int getPluginIndex(PluginDescriptor plugin)
548    {
549        return plugins.indexOf(plugin);
550    }
551
552    protected int getPluginModelIndex(PluginDescriptor plugin)
553    {
554        return getPluginIndex(plugin);
555    }
556
557    protected int getPluginTableIndex(PluginDescriptor plugin)
558    {
559        return getPluginTableIndex(getPluginModelIndex(plugin));
560    }
561
562    protected int getPluginIndex(String pluginClassName)
563    {
564        for (int i = 0; i < plugins.size(); i++)
565        {
566            final PluginDescriptor plugin = plugins.get(i);
567
568            if (plugin.getClassName().equals(pluginClassName))
569                return i;
570        }
571
572        return -1;
573    }
574
575    protected int getPluginModelIndex(String pluginClassName)
576    {
577        return getPluginIndex(pluginClassName);
578    }
579
580    protected int getPluginTableIndex(String pluginClassName)
581    {
582        return getPluginTableIndex(getPluginModelIndex(pluginClassName));
583    }
584
585    List<PluginDescriptor> getSelectedPlugins()
586    {
587        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
588
589        final int[] rows = table.getSelectedRows();
590        if (rows.length == 0)
591            return result;
592
593        final List<PluginDescriptor> cachedPlugins = plugins;
594
595        for (int i = 0; i < rows.length; i++)
596        {
597            try
598            {
599                final int index = table.convertRowIndexToModel(rows[i]);
600                if (index < cachedPlugins.size())
601                    result.add(cachedPlugins.get(index));
602            }
603            catch (IndexOutOfBoundsException e)
604            {
605                // ignore as async process can cause it
606            }
607        }
608
609        return result;
610    }
611
612    /**
613     * Select the specified list of ROI in the ROI Table
614     */
615    void setSelectedPlugins(HashSet<PluginDescriptor> newSelected)
616    {
617        final List<PluginDescriptor> modelPlugins = plugins;
618        final ListSelectionModel selectionModel = table.getSelectionModel();
619
620        // start selection change
621        selectionModel.setValueIsAdjusting(true);
622        try
623        {
624            // start by clearing selection
625            selectionModel.clearSelection();
626
627            for (int i = 0; i < modelPlugins.size(); i++)
628            {
629                final PluginDescriptor plugin = modelPlugins.get(i);
630
631                // HashSet provide fast "contains"
632                if (newSelected.contains(plugin))
633                {
634                    try
635                    {
636                        // convert model index to view index
637                        final int ind = table.convertRowIndexToView(i);
638                        if (ind != -1)
639                            selectionModel.addSelectionInterval(ind, ind);
640                    }
641                    catch (IndexOutOfBoundsException e)
642                    {
643                        // ignore
644                    }
645                }
646            }
647        }
648        finally
649        {
650            // end selection change
651            selectionModel.setValueIsAdjusting(false);
652        }
653    }
654
655    protected void refreshPluginsInternal()
656    {
657        plugins = filterList(getPlugins(), filter.getText());
658        // refresh table data
659        refreshTableData();
660    }
661
662    protected final void refreshPlugins()
663    {
664        ThreadUtil.runSingle(pluginsListRefresher);
665    }
666
667    protected void updateButtonsStateInternal()
668    {
669        final List<PluginDescriptor> selectedPlugins = getSelectedPlugins();
670        final boolean singleSelection = (selectedPlugins.size() == 1);
671        final PluginDescriptor singlePlugin = singleSelection ? selectedPlugins.get(0) : null;
672
673        detailButton.setEnabled(singleSelection);
674        documentationButton.setEnabled(singleSelection && !StringUtil.isEmpty(singlePlugin.getWeb()));
675    }
676
677    protected final void updateButtonsState()
678    {
679        ThreadUtil.runSingle(buttonsStateUpdater);
680    }
681
682    protected void updateRepositoriesInternal()
683    {
684        // final RepositoryPreferencePanel panel = (RepositoryPreferencePanel)
685        // getPreferencePanel(RepositoryPreferencePanel.class);
686        // // refresh repositories list (use list from GUI)
687        // final ArrayList<RepositoryInfo> repositeries = panel.repositories;
688
689        // refresh repositories list
690        final List<RepositoryInfo> repositeries = RepositoryPreferences.getRepositeries();
691        final RepositoryInfo savedRepository = (RepositoryInfo) repository.getSelectedItem();
692
693        // needed to disable events during update time
694        repository.removeActionListener(repositoryActionListener);
695
696        repository.removeAllItems();
697        for (RepositoryInfo repos : repositeries)
698            if (repos.isEnabled())
699                repository.addItem(repos);
700
701        repository.addActionListener(repositoryActionListener);
702
703        boolean selected = false;
704
705        // try to set back the old selected repository
706        if (savedRepository != null)
707        {
708            final String repositoryName = savedRepository.getName();
709
710            for (int ind = 0; ind < repository.getItemCount(); ind++)
711            {
712                final RepositoryInfo repo = (RepositoryInfo) repository.getItemAt(ind);
713
714                if ((repo != null) && (repo.getName().equals(repositoryName)))
715                {
716                    repository.setSelectedIndex(ind);
717                    selected = true;
718                    break;
719                }
720            }
721        }
722
723        // manually launch the action
724        if (!selected)
725            repository.setSelectedIndex((repository.getItemCount() > 0) ? 0 : -1);
726
727        // avoid automatic minimum size here
728        repository.setMinimumSize(new Dimension(48, 18));
729    }
730
731    protected final void updateRepositories()
732    {
733        ThreadUtil.runSingle(repositoriesUpdater);
734    }
735
736    protected void refreshTableDataInternal()
737    {
738        final List<PluginDescriptor> plugins = getSelectedPlugins();
739
740        try
741        {
742            tableModel.fireTableDataChanged();
743        }
744        catch (Throwable t)
745        {
746            // sometime sorting can throw exception, ignore them...
747        }
748
749        // restore previous selected plugins if possible
750        setSelectedPlugins(new HashSet<PluginDescriptor>(plugins));
751        // update button state
752        buttonsStateUpdater.run();
753    }
754
755    protected final void refreshTableData()
756    {
757        ThreadUtil.runSingle(tableDataRefresher);
758    }
759
760    protected void pluginsChanged()
761    {
762        refreshPlugins();
763    }
764
765    @Override
766    protected void load()
767    {
768
769    }
770
771    @Override
772    protected void save()
773    {
774        // reload repositories as some parameter as beta flag can have changed
775        updateRepositories();
776    }
777
778    @Override
779    public void textChanged(IcyTextField source, boolean validate)
780    {
781        pluginsChanged();
782    }
783
784    @Override
785    public void valueChanged(ListSelectionEvent e)
786    {
787        final int selected = table.getSelectedRow();
788
789        if (!e.getValueIsAdjusting() && (selected != -1))
790        {
791            final int fi = e.getFirstIndex();
792            final int li = e.getLastIndex();
793
794            if ((fi == -1) || ((fi <= selected) && (li >= selected)))
795                updateButtonsState();
796        }
797    }
798}