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.inspector;
020
021import icy.action.CanvasActions;
022import icy.canvas.CanvasLayerEvent;
023import icy.canvas.CanvasLayerListener;
024import icy.canvas.IcyCanvas;
025import icy.canvas.Layer;
026import icy.gui.component.IcyTextField;
027import icy.gui.component.IcyTextField.TextChangeListener;
028import icy.gui.component.editor.VisibleCellEditor;
029import icy.gui.component.renderer.VisibleCellRenderer;
030import icy.gui.main.ActiveViewerListener;
031import icy.gui.viewer.Viewer;
032import icy.gui.viewer.ViewerEvent;
033import icy.gui.viewer.ViewerEvent.ViewerEventType;
034import icy.system.thread.ThreadUtil;
035import icy.util.StringUtil;
036
037import java.awt.BorderLayout;
038import java.awt.event.KeyEvent;
039import java.util.ArrayList;
040import java.util.List;
041
042import javax.swing.ActionMap;
043import javax.swing.InputMap;
044import javax.swing.JComponent;
045import javax.swing.JPanel;
046import javax.swing.JScrollPane;
047import javax.swing.KeyStroke;
048import javax.swing.ListSelectionModel;
049import javax.swing.ScrollPaneConstants;
050import javax.swing.event.ListSelectionEvent;
051import javax.swing.event.ListSelectionListener;
052import javax.swing.table.AbstractTableModel;
053
054import org.jdesktop.swingx.JXTable;
055import org.jdesktop.swingx.decorator.HighlighterFactory;
056import org.jdesktop.swingx.table.ColumnControlButton;
057import org.jdesktop.swingx.table.TableColumnExt;
058
059/**
060 * @author Stephane
061 */
062public class LayersPanel extends JPanel implements ActiveViewerListener, CanvasLayerListener, TextChangeListener,
063        ListSelectionListener
064{
065    private class CanvasRefresher implements Runnable
066    {
067        IcyCanvas newCanvas;
068
069        public CanvasRefresher()
070        {
071            super();
072        }
073
074        @Override
075        public void run()
076        {
077            final IcyCanvas c = newCanvas;
078
079            // change canvas
080            if (canvas != c)
081            {
082                if (canvas != null)
083                    canvas.removeLayerListener(LayersPanel.this);
084
085                canvas = c;
086
087                if (canvas != null)
088                    canvas.addLayerListener(LayersPanel.this);
089            }
090
091            refreshLayersInternal();
092        }
093    }
094
095    /**
096     * 
097     */
098    private static final long serialVersionUID = 4550426171735455449L;
099
100    static final String[] columnNames = {"Name", ""};
101
102    List<Layer> layers;
103    IcyCanvas canvas;
104
105    // GUI
106    AbstractTableModel tableModel;
107    ListSelectionModel tableSelectionModel;
108    JXTable table;
109    IcyTextField nameFilter;
110    LayerControlPanel controlPanel;
111
112    // internals
113    boolean isSelectionAdjusting;
114    boolean isLayerEditing;
115    boolean isLayerPropertiesAdjusting;
116
117    final Runnable layersRefresher;
118    final Runnable tableDataRefresher;
119    final Runnable controlPanelRefresher;
120    final CanvasRefresher canvasRefresher;
121
122    public LayersPanel()
123    {
124        super();
125
126        layers = new ArrayList<Layer>();
127        canvas = null;
128        isSelectionAdjusting = false;
129        isLayerEditing = false;
130        isLayerPropertiesAdjusting = false;
131
132        layersRefresher = new Runnable()
133        {
134            @Override
135            public void run()
136            {
137                refreshLayersInternal();
138            }
139        };
140        tableDataRefresher = new Runnable()
141        {
142            @Override
143            public void run()
144            {
145                refreshTableData();
146            }
147        };
148        controlPanelRefresher = new Runnable()
149        {
150            @Override
151            public void run()
152            {
153                controlPanel.refresh();
154            }
155        };
156        canvasRefresher = new CanvasRefresher();
157
158        // build GUI
159        initialize();
160
161        // build table
162        tableModel = new AbstractTableModel()
163        {
164            /**
165             * 
166             */
167            private static final long serialVersionUID = -8573364273165723214L;
168
169            @Override
170            public int getColumnCount()
171            {
172                return columnNames.length;
173            }
174
175            @Override
176            public String getColumnName(int column)
177            {
178                return columnNames[column];
179            }
180
181            @Override
182            public int getRowCount()
183            {
184                return layers.size();
185            }
186
187            @Override
188            public Object getValueAt(int row, int column)
189            {
190                // safe
191                if (row >= layers.size())
192                    return null;
193
194                final Layer layer = layers.get(row);
195
196                switch (column)
197                {
198                    case 0:
199                        // layer name
200                        return layer.getName();
201
202                    case 1:
203                        // layer visibility
204                        return Boolean.valueOf(layer.isVisible());
205
206                    default:
207                        return "";
208                }
209            }
210
211            @Override
212            public void setValueAt(Object value, int row, int column)
213            {
214                // safe
215                if (row >= layers.size())
216                    return;
217
218                isLayerEditing = true;
219                try
220                {
221                    final Layer layer = layers.get(row);
222
223                    switch (column)
224                    {
225                        case 0:
226                            layer.setName((String) value);
227                            break;
228
229                        case 1:
230                            // layer visibility
231                            layer.setVisible(((Boolean) value).booleanValue());
232                            break;
233                    }
234                }
235                finally
236                {
237                    isLayerEditing = false;
238                }
239            }
240
241            @Override
242            public boolean isCellEditable(int row, int column)
243            {
244                // safe
245                if (row >= layers.size())
246                    return false;
247
248                final boolean editable;
249
250                // name field ?
251                if (column == 0)
252                {
253                    final Layer layer = layers.get(row);
254                    editable = (layer != null) ? !layer.isReadOnly() : false;
255                }
256                else
257                    editable = true;
258
259                return editable;
260            }
261
262            @Override
263            public Class<?> getColumnClass(int columnIndex)
264            {
265                switch (columnIndex)
266                {
267                    default:
268                    case 0:
269                        // layer name
270                        return String.class;
271
272                    case 1:
273                        // layer visibility
274                        return Boolean.class;
275                }
276            }
277        };
278        // set table model
279        table.setModel(tableModel);
280        // alternate highlight
281        table.setHighlighters(HighlighterFactory.createSimpleStriping());
282        // disable extra actions from column control
283        ((ColumnControlButton) table.getColumnControl()).setAdditionalActionsVisible(false);
284        // remove the internal find command (we have our own filter)
285        table.getActionMap().remove("find");
286
287        TableColumnExt col;
288
289        // columns setting - name
290        col = table.getColumnExt(0);
291        col.setPreferredWidth(140);
292        col.setToolTipText("Layer name (double click in a cell to edit)");
293
294        // columns setting - visible
295        col = table.getColumnExt(1);
296        col.setPreferredWidth(20);
297        col.setMinWidth(20);
298        col.setMaxWidth(20);
299        col.setCellEditor(new VisibleCellEditor(18));
300        col.setCellRenderer(new VisibleCellRenderer(18));
301        col.setToolTipText("Make the layer visible or not");
302        col.setResizable(false);
303
304        // table selection model
305        tableSelectionModel = table.getSelectionModel();
306        tableSelectionModel.addListSelectionListener(this);
307        tableSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
308
309        // create shortcuts
310        buildActionMap();
311
312        // and refresh layers
313        refreshLayers();
314    }
315
316    private void initialize()
317    {
318        nameFilter = new IcyTextField();
319        nameFilter.setToolTipText("Enter a string sequence to filter Layer on name");
320        nameFilter.addTextChangeListener(this);
321
322        table = new JXTable();
323        table.setAutoStartEditOnKeyStroke(false);
324        table.setRowHeight(24);
325        table.setShowVerticalLines(false);
326        table.setColumnControlVisible(true);
327        table.setColumnSelectionAllowed(false);
328        table.setRowSelectionAllowed(true);
329        table.setAutoCreateRowSorter(true);
330
331        controlPanel = new LayerControlPanel(this);
332
333        setLayout(new BorderLayout(0, 0));
334        add(nameFilter, BorderLayout.NORTH);
335        add(new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
336                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER);
337        add(controlPanel, BorderLayout.SOUTH);
338
339        validate();
340    }
341
342    void buildActionMap()
343    {
344        final InputMap imap = table.getInputMap(JComponent.WHEN_FOCUSED);
345        final ActionMap amap = table.getActionMap();
346
347        imap.put(CanvasActions.unselectAction.getKeyStroke(), CanvasActions.unselectAction.getName());
348        imap.put(CanvasActions.deleteLayersAction.getKeyStroke(), CanvasActions.deleteLayersAction.getName());
349        // also allow backspace key for delete operation here
350        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), CanvasActions.deleteLayersAction.getName());
351
352        // disable search feature (we have our own filter)
353        amap.remove("find");
354        amap.put(CanvasActions.unselectAction.getName(), CanvasActions.unselectAction);
355        amap.put(CanvasActions.deleteLayersAction.getName(), CanvasActions.deleteLayersAction);
356    }
357
358    public void setNameFilter(String name)
359    {
360        nameFilter.setText(name);
361    }
362
363    /**
364     * refresh Layer list (and refresh table data according)
365     */
366    protected void refreshLayers()
367    {
368        ThreadUtil.runSingle(layersRefresher);
369    }
370
371    /**
372     * refresh layer list (internal)
373     */
374    void refreshLayersInternal()
375    {
376        if (canvas != null)
377            layers = filterList(canvas.getLayers(false), nameFilter.getText());
378        else
379            layers.clear();
380
381        // refresh table data
382        ThreadUtil.runSingle(tableDataRefresher);
383    }
384
385    /**
386     * Return index of specified Layer in the Layer list
387     */
388    protected int getLayerIndex(Layer layer)
389    {
390        return layers.indexOf(layer);
391    }
392
393    /**
394     * Return index of specified Layer in the model
395     */
396    protected int getLayerModelIndex(Layer layer)
397    {
398        return getLayerIndex(layer);
399    }
400
401    /**
402     * Return index of specified Layer in the table
403     */
404    protected int getLayerTableIndex(Layer layer)
405    {
406        final int ind = getLayerModelIndex(layer);
407
408        if (ind == -1)
409            return ind;
410
411        try
412        {
413            return table.convertRowIndexToView(ind);
414        }
415        catch (IndexOutOfBoundsException e)
416        {
417            return -1;
418        }
419    }
420
421    // public Layer getFirstSelectedRoi()
422    // {
423    // int index = table.getSelectedRow();
424    //
425    // if (index != -1)
426    // {
427    // try
428    // {
429    // index = table.convertRowIndexToModel(index);
430    // }
431    // catch (IndexOutOfBoundsException e)
432    // {
433    // // ignore
434    // }
435    //
436    // if ((index >= 0) || (index < layers.size()))
437    // return layers.get(index);
438    // }
439    //
440    // return null;
441    // }
442
443    public ArrayList<Layer> getSelectedLayers()
444    {
445        final ArrayList<Layer> result = new ArrayList<Layer>();
446
447        for (int rowIndex : table.getSelectedRows())
448        {
449            int index = -1;
450
451            if (rowIndex != -1)
452            {
453                try
454                {
455                    index = table.convertRowIndexToModel(rowIndex);
456                }
457                catch (IndexOutOfBoundsException e)
458                {
459                    // ignore
460                }
461            }
462
463            if ((index >= 0) && (index < layers.size()))
464                result.add(layers.get(index));
465        }
466
467        return result;
468    }
469
470    public void clearSelected()
471    {
472        setSelectedLayersInternal(new ArrayList<Layer>());
473    }
474
475    void setSelectedLayersInternal(List<Layer> newSelected)
476    {
477        isSelectionAdjusting = true;
478        try
479        {
480            table.clearSelection();
481
482            if (newSelected != null)
483            {
484                for (Layer layer : newSelected)
485                {
486                    final int index = getLayerTableIndex(layer);
487
488                    if (index > -1)
489                        tableSelectionModel.addSelectionInterval(index, index);
490                }
491            }
492        }
493        finally
494        {
495            isSelectionAdjusting = false;
496        }
497
498        // notify selection changed
499        selectionChanged();
500    }
501
502    List<Layer> filterList(List<Layer> list, String nameFilterText)
503    {
504        final List<Layer> result = new ArrayList<Layer>();
505
506        final boolean nameEmpty = StringUtil.isEmpty(nameFilterText, true);
507        final String nameFilterUp;
508
509        if (!nameEmpty)
510            nameFilterUp = nameFilterText.trim().toLowerCase();
511        else
512            nameFilterUp = "";
513
514        for (Layer layer : list)
515        {
516            // search in name and type
517            if (nameEmpty || (layer.getName().toLowerCase().indexOf(nameFilterUp) != -1))
518                result.add(layer);
519        }
520
521        return result;
522    }
523
524    protected void refreshTableData()
525    {
526        final List<Layer> save = getSelectedLayers();
527
528        // need to be done on EDT
529        ThreadUtil.invokeNow(new Runnable()
530        {
531            @Override
532            public void run()
533            {
534                isSelectionAdjusting = true;
535                try
536                {
537                    tableModel.fireTableDataChanged();
538                }
539                finally
540                {
541                    isSelectionAdjusting = false;
542                }
543
544                setSelectedLayersInternal(save);
545            }
546        });
547    }
548
549    // protected void refreshTableRow(final Layer layer)
550    // {
551    // isSelectionAdjusting = true;
552    // try
553    // {
554    // final int rowIndex = getLayerModelIndex(layer);
555    //
556    // tableModel.fireTableRowsUpdated(rowIndex, rowIndex);
557    // }
558    // finally
559    // {
560    // isSelectionAdjusting = false;
561    // }
562    //
563    // // restore selected layer
564    // if (sequence != null)
565    // setSelectedLayersInternal(sequence.getSelectedROIs());
566    // else
567    // setSelectedLayersInternal(null);
568    //
569    // // refresh control panel
570    // refreshControlPanel();
571    // }
572
573    /**
574     * Called when selection changed
575     */
576    protected void selectionChanged()
577    {
578        // refresh control panel
579        ThreadUtil.runSingle(controlPanelRefresher);
580    }
581
582    @Override
583    public void textChanged(IcyTextField source, boolean validate)
584    {
585        if (source == nameFilter)
586            refreshLayers();
587    }
588
589    @Override
590    public void valueChanged(ListSelectionEvent e)
591    {
592        // internal change --> ignore
593        if (isSelectionAdjusting || e.getValueIsAdjusting())
594            return;
595
596        selectionChanged();
597    }
598
599    @Override
600    public void viewerActivated(Viewer viewer)
601    {
602        if (viewer != null)
603            canvasRefresher.newCanvas = viewer.getCanvas();
604        else
605            canvasRefresher.newCanvas = null;
606
607        ThreadUtil.runSingle(canvasRefresher);
608    }
609
610    @Override
611    public void viewerDeactivated(Viewer viewer)
612    {
613        // nothing here
614    }
615
616    @Override
617    public void activeViewerChanged(ViewerEvent event)
618    {
619        if (event.getType() == ViewerEventType.CANVAS_CHANGED)
620        {
621            canvasRefresher.newCanvas = event.getSource().getCanvas();
622            ThreadUtil.runSingle(canvasRefresher);
623        }
624    }
625
626    @Override
627    public void canvasLayerChanged(CanvasLayerEvent event)
628    {
629        // refresh layer from externals changes
630        if (isLayerEditing)
631            return;
632
633        switch (event.getType())
634        {
635            case ADDED:
636            case REMOVED:
637                refreshLayers();
638                break;
639
640            case CHANGED:
641                final String property = event.getProperty();
642
643                if (Layer.PROPERTY_NAME.equals(property) || Layer.PROPERTY_OPACITY.equals(property)
644                        || Layer.PROPERTY_VISIBLE.equals(property))
645                    // refresh table data
646                    ThreadUtil.runSingle(tableDataRefresher);
647                break;
648        }
649    }
650
651}