001package jmri.jmrit.catalog;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Dimension;
006import java.awt.FlowLayout;
007import java.awt.Font;
008import java.awt.Frame;
009import java.awt.GridBagConstraints;
010import java.awt.GridBagLayout;
011import java.awt.Insets;
012import java.awt.datatransfer.DataFlavor;
013import java.awt.event.ActionEvent;
014import java.awt.event.ActionListener;
015import java.awt.image.BufferedImage;
016import java.io.File;
017import java.util.ArrayList;
018
019import javax.swing.Box;
020import javax.swing.BoxLayout;
021import javax.swing.JButton;
022import javax.swing.JComboBox;
023import javax.swing.JDialog;
024import javax.swing.JLabel;
025import javax.swing.JPanel;
026import javax.swing.JScrollPane;
027import javax.swing.JSeparator;
028import javax.swing.JTextField;
029import jmri.InstanceManager;
030import jmri.util.swing.DrawSquares;
031import jmri.util.swing.ImagePanel;
032import jmri.util.swing.JmriJOptionPane;
033
034import org.apache.commons.io.FilenameUtils;
035
036/**
037 * Create a Dialog to display the images in a file system directory.
038 * <p>
039 * PreviewDialog is not modal to allow dragNdrop of icons from it to catalog
040 * panels and functioning of the catalog panels without dismissing this dialog.
041 * Component is used in {@link jmri.jmrit.catalog.DirectorySearcher}, accessed
042 * from {@link jmri.jmrit.catalog.ImageIndexEditor} File menu items.
043 *
044 * @author Pete Cressman Copyright 2009
045 * @author Egbert Broerse Copyright 2017
046 */
047public class PreviewDialog extends JDialog {
048
049    JPanel _selectedImage;
050    static Color _grayColor = new Color(235, 235, 235);
051    static Color _darkGrayColor = new Color(150, 150, 150);
052    protected Color[] colorChoice = new Color[]{Color.white, _grayColor, _darkGrayColor};
053    /**
054     * Active base color for Preview background, copied from active Panel where
055     * available.
056     */
057    protected BufferedImage[] _backgrounds;
058
059    JLabel _previewLabel = new JLabel();
060    protected ImagePanel _preview;
061    protected JScrollPane js;
062
063    int _cnt;           // number of files displayed when setIcons() method runs
064    int _startNum;      // total number of files displayed from a directory
065    boolean needsMore = true;
066
067    File _currentDir;   // current FS directory
068    String[] _filter;   // file extensions of types to display
069    ActionListener _lookAction;
070
071    /**
072     *
073     * @param frame  JFrame on screen to center this dialog over
074     * @param title  title for the frame
075     * @param dir    starting icon file directory
076     * @param filter file patterns to display in icon tree
077     */
078    protected PreviewDialog(Frame frame, String title, File dir, String[] filter) {
079        super(frame, Bundle.getMessage(title), false);
080        _currentDir = dir;
081        _filter = new String[filter.length];
082        for (int i = 0; i < filter.length; i++) {
083            _filter[i] = filter[i];
084        }
085    }
086
087    protected void init(ActionListener moreAction, ActionListener lookAction, ActionListener cancelAction, int startNum) {
088        if (log.isDebugEnabled()) {
089            log.debug("Enter _previewDialog.init dir= {}", _currentDir.getPath());
090        }
091        addWindowListener(new java.awt.event.WindowAdapter() {
092            @Override
093            public void windowClosing(java.awt.event.WindowEvent e) {
094                InstanceManager.getDefault(DirectorySearcher.class).close();
095                dispose();
096            }
097        });
098        JPanel pTop = new JPanel();
099        pTop.setLayout(new BoxLayout(pTop, BoxLayout.Y_AXIS));
100        pTop.add(new JLabel(_currentDir.getPath()));
101        JTextField msg = new JTextField();
102        msg.setFont(new Font("Dialog", Font.BOLD, 12));
103        msg.setEditable(false);
104        msg.setBackground(pTop.getBackground());
105        pTop.add(msg);
106        getContentPane().add(pTop, BorderLayout.NORTH);
107
108        JPanel p = new JPanel();
109        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
110        p.add(Box.createHorizontalStrut(5));
111
112        JPanel previewPanel = setupPanel(); // provide panel for images, add to bottom of window
113        _startNum = startNum;
114        needsMore = setIcons(startNum);
115        if (_noMemory) {
116            int choice = JmriJOptionPane.showOptionDialog(this,
117                    Bundle.getMessage("OutOfMemory", _cnt), Bundle.getMessage("ErrorTitle"),
118                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
119                    new String[]{Bundle.getMessage("ButtonStop"), Bundle.getMessage("ShowContents")}, 1);
120            if (choice != 1) { // showcontents not selected
121                return;
122            }
123        }
124
125        if (needsMore) {
126            if (moreAction != null) {
127                p.add(Box.createHorizontalStrut(5));
128                JButton moreButton = new JButton(Bundle.getMessage("ButtonDisplayMore"));
129                moreButton.addActionListener(moreAction);
130                moreButton.setVisible(needsMore);
131                p.add(moreButton);
132            } else {
133                log.error("More ActionListener missing");
134            }
135            msg.setText(Bundle.getMessage("moreMsg", Bundle.getMessage("ButtonDisplayMore")));
136        }
137
138        boolean hasButtons = needsMore;
139        msg.setText(Bundle.getMessage("dragMsg"));
140
141        _lookAction = lookAction;
142        if (lookAction != null) {
143            p.add(Box.createHorizontalStrut(5));
144            JButton lookButton = new JButton(Bundle.getMessage("ButtonKeepLooking"));
145            lookButton.addActionListener(lookAction);
146            p.add(lookButton);
147            hasButtons = true;
148        }
149
150        JPanel panel = new JPanel();
151        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
152
153        if (hasButtons) {
154            p.add(Box.createHorizontalStrut(5));
155            JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
156            cancelButton.addActionListener(cancelAction);
157            p.add(cancelButton);
158            p.add(Box.createHorizontalStrut(5));
159            p.setPreferredSize(new Dimension(400, cancelButton.getPreferredSize().height));
160            panel.add(p);
161            panel.add(new JSeparator());
162        }
163
164        panel.add(previewPanel);
165        getContentPane().add(panel);
166        setLocationRelativeTo(null);
167        pack();
168        setVisible(true);
169    }
170
171    ActionListener getLookActionListener() {
172        return _lookAction;
173    }
174
175    /**
176     * Set up a display panel to display icons. Includes a "View on:" drop down
177     * list. Employs a normal JComboBox, no Panel Background option.
178     *
179     * @return a JPanel with preview pane and background color drop down
180     */
181    private JPanel setupPanel() {
182        JPanel previewPanel = new JPanel();
183        previewPanel.setLayout(new BoxLayout(previewPanel, BoxLayout.Y_AXIS));
184        JPanel p = new JPanel();
185        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
186        p.add(_previewLabel);
187        previewPanel.add(p);
188        _preview = new ImagePanel();
189        log.debug("Preview ImagePanel created");
190        _preview.setLayout(new BoxLayout(_preview, BoxLayout.Y_AXIS));
191        _preview.setOpaque(false);
192        js = new JScrollPane(_preview);
193        previewPanel.add(js);
194
195        // create array of backgrounds
196        if (_backgrounds == null) {
197            _backgrounds = new BufferedImage[4];
198            for (int i = 0; i <= 2; i++) {
199                _backgrounds[i] = DrawSquares.getImage(300, 400, 10, colorChoice[i], colorChoice[i]);
200            }
201            _backgrounds[3] = DrawSquares.getImage(300, 400, 10, Color.white, _grayColor);
202        }
203        // create background selection combo box
204        JComboBox<String> bgColorBox = new JComboBox<>();
205        bgColorBox.addItem(Bundle.getMessage("White"));
206        bgColorBox.addItem(Bundle.getMessage("LightGray"));
207        bgColorBox.addItem(Bundle.getMessage("DarkGray"));
208        bgColorBox.addItem(Bundle.getMessage("Checkers")); // checkers option, under development
209        bgColorBox.setSelectedIndex(0); // white
210        bgColorBox.addActionListener((ActionEvent e) -> {
211            // load background image
212            _preview.setImage(_backgrounds[bgColorBox.getSelectedIndex()]);
213            log.debug("Preview setImage called");
214            _preview.setOpaque(false);
215            // _preview.repaint(); // force redraw
216            _preview.invalidate();
217        });
218
219        JPanel pp = new JPanel();
220        pp.setLayout(new FlowLayout(FlowLayout.CENTER));
221        pp.add(new JLabel(Bundle.getMessage("setBackground")));
222        pp.add(bgColorBox);
223        previewPanel.add(pp);
224
225        return previewPanel;
226    }
227
228    void resetPanel() {
229        _selectedImage = null;
230        if (_preview == null) {
231            return;
232        }
233        log.debug("resetPanel");
234        _preview.removeAll();
235        _preview.setImage(_backgrounds[0]);
236        _preview.invalidate();
237        pack();
238    }
239
240    protected int getNumFilesShown() {
241        return _startNum + _cnt;
242    }
243
244    class MemoryExceptionHandler implements Thread.UncaughtExceptionHandler {
245
246        @Override
247        public void uncaughtException(Thread t, Throwable e) {
248            _noMemory = true;
249            log.error("MemoryExceptionHandler: {} {} files read from directory {}", e, _cnt, _currentDir);
250            if (log.isDebugEnabled()) {
251                log.debug("memoryAvailable = {}", availableMemory());
252            }
253        }
254    }
255
256    boolean _noMemory = false;
257
258    /**
259     * Display (thumbnails if image is large) of the current directory. Number
260     * of images displayed may be restricted due to memory constraints.
261     *
262     * @return true if memory limits displaying all the images
263     */
264    private boolean setIcons(int startNum) {
265        Thread.UncaughtExceptionHandler exceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
266        // VM launches another thread to run ImageFetcher.
267        // This handler will catch memory exceptions from that thread
268        _noMemory = false;
269        Thread.setDefaultUncaughtExceptionHandler(new MemoryExceptionHandler());
270        // allow room for ImageFetcher threads
271        log.debug("setIcons: startNum= {}", startNum);
272        GridBagLayout gridbag = new GridBagLayout();
273        _preview.setLayout(gridbag);
274        GridBagConstraints c = new GridBagConstraints();
275        c.fill = GridBagConstraints.NONE;
276        c.anchor = GridBagConstraints.CENTER;
277        c.weightx = 1.0;
278        c.weighty = 1.0;
279        c.gridy = -1;
280        c.gridx = 0;
281        _cnt = 0;       // number of images displayed in this panel
282        int cnt = 0;    // total number of images in directory
283        File[] files = _currentDir.listFiles(); // all files, filtered below
284
285        if (files != null) { // prevent spotbugs NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE
286
287            // sort list of files alphabetically
288            ArrayList<File> aList = new ArrayList<>(java.util.Arrays.asList(files));
289            java.util.Collections.sort(aList);
290            files = aList.toArray(files);
291
292            int nCols = 1;
293            int nRows = 1;
294            int nAvail = 1;
295
296            long memoryAvailable = availableMemory();
297            long memoryUsed = 0;        // estimate
298            for (int i = 0; i < files.length; i++) {
299                String ext = FilenameUtils.getExtension(files[i].getName());
300                for (int k = 0; k < _filter.length; k++) {
301                    if (ext != null && ext.equalsIgnoreCase(_filter[k])) {
302                        // files[i] filtered to be an image file
303                        if (cnt < startNum) {
304                            cnt++;
305                            continue;
306                        }
307                        String name = files[i].getName();
308                        int index = name.indexOf('.');
309                        if (index > 0) {
310                            name = name.substring(0, index);
311                        }
312                        String path = files[i].getAbsolutePath();
313                        NamedIcon icon = new NamedIcon(path, name);
314                        long size = icon.getIconWidth() * icon.getIconHeight();
315                        log.debug("Memory calculation icon size= {} memoryAvailable= {} memoryUsed= {}", size, memoryAvailable, memoryUsed);
316
317                        if (memoryAvailable < 4 * size) {
318                            _noMemory = true;
319                            log.debug("Memory calculation caught icon size= {} testSize= {} memoryAvailable= {}", size, 4 * size, memoryAvailable);
320                            break;
321                        }
322                        double scale = icon.reduceTo(CatalogPanel.ICON_WIDTH,
323                                CatalogPanel.ICON_HEIGHT, CatalogPanel.ICON_SCALE);
324                        if (_noMemory) {
325                            log.debug("MemoryExceptionHandler caught icon size={} ", size);
326                            break;
327                        }
328                        if (scale < 1.0) {
329                            size *= 4;
330                        } else {
331                            size += 1000;
332                        }
333                        memoryUsed += size;
334                        memoryAvailable -= size;
335                        _cnt++;
336                        cnt++;
337                        if (_cnt > nAvail) {
338                            nCols++;
339                            nRows++;
340                            nAvail = nCols * nRows;
341                            c.gridx = nCols - 1;
342                            c.gridy = 0;
343                        } else if (_cnt > nAvail - nRows) {
344                            if (c.gridx < nCols - 1) {
345                                c.gridx++;
346                            } else {
347                                c.gridx = 0;
348                                c.gridy++;
349                            }
350                        } else {
351                            c.gridy++;
352                        }
353
354                        c.insets = new Insets(5, 5, 0, 0);
355                        JLabel image;
356                        try {
357                            image = new DragJLabel(new DataFlavor(ImageIndexEditor.IconDataFlavorMime));
358                        } catch (java.lang.ClassNotFoundException cnfe) {
359                            log.error("Unable to find class supporting {}", ImageIndexEditor.IconDataFlavorMime, cnfe);
360                            image = new JLabel(cnfe.getMessage());
361                        }
362                        image.setOpaque(false);
363                        image.setName(name);
364                        image.setIcon(icon);
365                        JPanel p = new JPanel();
366                        p.setOpaque(false);
367                        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
368                        p.add(image);
369                        if (name.length() > 18) {
370                            name = name.substring(0, 18);
371                        }
372                        JLabel nameLabel = new JLabel(name);
373                        nameLabel.setOpaque(false);
374                        JLabel label = new JLabel(Bundle.getMessage("scale", CatalogPanel.printDbl(scale, 2)));
375                        label.setOpaque(false);
376                        p.add(label);
377                        p.add(nameLabel);
378                        gridbag.setConstraints(p, c);
379                        log.debug("{} inserted at ({}, {})", name, c.gridx, c.gridy);
380                        _preview.add(p);
381                    }
382                    if (_noMemory) {
383                        break;
384                    }
385                }
386            }
387            c.gridy++;
388            c.gridx++;
389        }
390        JLabel bottom = new JLabel();
391        gridbag.setConstraints(bottom, c);
392        _preview.add(bottom);
393        String msg = Bundle.getMessage("numImagesInDir", _currentDir.getName(), DirectorySearcher.numImageFiles(_currentDir));
394        if (startNum > 0) {
395            msg = Bundle.getMessage("numImagesShown", msg, _cnt, startNum);
396        }
397        _previewLabel.setText(msg);
398        CatalogPanel.packParentFrame(this);
399
400        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
401        return _noMemory;
402    }
403
404    static int CHUNK = 500000;
405
406    private long availableMemory() {
407        return Runtime.getRuntime().freeMemory()/2;
408    }
409
410    @Override
411    public void dispose() {
412        if (_preview != null) {
413            resetPanel();
414        }
415        this.removeAll();
416        _preview = null;
417        super.dispose();
418        log.debug("PreviewDialog disposed.");
419    }
420
421    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PreviewDialog.class);
422
423}