001package jmri.jmrit.catalog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.Color;
006import java.awt.event.ActionEvent;
007import java.awt.event.ActionListener;
008import java.io.File;
009
010import javax.swing.BorderFactory;
011import javax.swing.BoxLayout;
012import javax.swing.JFileChooser;
013import javax.swing.JFrame;
014import javax.swing.JLabel;
015import javax.swing.JPanel;
016import javax.swing.filechooser.FileNameExtensionFilter;
017import javax.swing.filechooser.FileSystemView;
018
019import jmri.CatalogTreeManager;
020import jmri.InstanceManager;
021import jmri.InstanceManagerAutoDefault;
022import jmri.util.ThreadingUtil;
023import jmri.util.swing.JmriJOptionPane;
024
025import org.apache.commons.io.FilenameUtils;
026
027/**
028 * A file system directory searcher to locate Image files to include in an Image
029 * Catalog.
030 *
031 * @author Pete Cressman Copyright 2010
032 */
033public class DirectorySearcher implements InstanceManagerAutoDefault {
034
035    // For choosing image directories
036    private JFileChooser _directoryChooser = null;
037
038    PreviewDialog _previewDialog = null;
039    Seacher _searcher;
040    JFrame _waitDialog;
041    JLabel _waitText;
042
043    public DirectorySearcher() {
044    }
045
046    public static DirectorySearcher instance() {
047        return InstanceManager.getDefault(DirectorySearcher.class);
048    }
049
050    /**
051     * Open file anywhere in the file system and let the user decide whether to
052     * add it to the Catalog.
053     *
054     * @param msg     Bundle property key (string) for i18n title string
055     * @param recurse if directory choice has no images, set chooser to sub
056     *                directory so user can continue looking
057     * @return chosen directory or null to cancel operation
058     */
059    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification="false postive, guarded by logic")
060    private File getDirectory(String msg, boolean recurse) {
061        if (_directoryChooser == null) {
062            _directoryChooser = new jmri.util.swing.JmriJFileChooser(FileSystemView.getFileSystemView());
063            _directoryChooser.setFileFilter(new FileNameExtensionFilter("Graphics Files", CatalogTreeManager.IMAGE_FILTER)); // NOI18N
064        }
065        _directoryChooser.setDialogTitle(Bundle.getMessage(msg));
066        _directoryChooser.rescanCurrentDirectory();
067        _directoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
068
069        while (true) {
070            int retVal = _directoryChooser.showOpenDialog(null);
071            if (retVal != JFileChooser.APPROVE_OPTION) {
072                return null;  // give up if no file selected
073            }
074            File dir = _directoryChooser.getSelectedFile();
075            if (dir != null) {
076                if (!recurse) {
077                    return dir;
078                }
079                int cnt = numImageFiles(dir);
080                if (cnt > 0) {
081                    return dir;
082                } else {
083                    int choice = JmriJOptionPane.showOptionDialog(null,
084                            Bundle.getMessage("NoImagesInDir", dir), Bundle.getMessage("QuestionTitle"),
085                            JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null,
086                            new String[]{Bundle.getMessage("ButtonStop"), Bundle.getMessage("ButtonKeepLooking")}, 1);
087                    switch (choice) {
088                        case 0: // stop
089                            return null;
090                        case 1: // keep looking
091                            _directoryChooser.setCurrentDirectory(dir);
092                            break;
093                        default:
094                            return dir;
095                    }
096                }
097            }
098        }
099    }
100
101    protected static int numImageFiles(File dir) {
102        File[] files = dir.listFiles();
103        if (files == null) {
104            return 0;
105        }
106        int count = 0;
107        for (int i = 0; i < files.length; i++) {
108            String ext = FilenameUtils.getExtension(files[i].getName());
109            for (int k = 0; k < CatalogTreeManager.IMAGE_FILTER.length; k++) {
110                if (ext != null && ext.equalsIgnoreCase(CatalogTreeManager.IMAGE_FILTER[k])) {
111                    count++; // OK directory has image files
112                }
113            }
114        }
115        return count;
116    }
117
118    private void showWaitFrame(String msgkey, File dir) {
119        if (_waitDialog == null) {
120            _waitDialog = new JFrame();
121            _waitDialog.setUndecorated(true);
122            JPanel panel = new JPanel();
123            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
124            panel.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2, true));
125            panel.add(new JLabel(Bundle.getMessage("waitWarning")));
126
127            _waitText = new JLabel();
128            panel.add(_waitText);
129            panel.setBackground(_waitText.getBackground());
130
131            _waitDialog.getContentPane().add(panel);
132            _waitDialog.setLocationRelativeTo(null);
133            _waitDialog.setVisible(false);
134        }
135        if (dir != null) {
136            _waitText.setText(Bundle.getMessage(msgkey, dir.getName()));
137            _waitDialog.setVisible(true);
138            _waitDialog.pack();
139            _waitDialog.toFront();
140        }
141    }
142
143    private void closeWaitFrame() {
144        if (_waitDialog != null) {
145            _waitDialog.dispose();
146            _waitDialog = null;
147        }
148    }
149
150    private void clearSearch() {
151        if (_previewDialog != null) {
152            _previewDialog.dispose();
153        }
154        if (_searcher != null) {
155            synchronized (_searcher) {
156                _searcher.notify();
157            }
158        }
159
160    }
161
162    /**
163     * Open one directory.
164     *
165     */
166    public void openDirectory() {
167        clearSearch();
168        File dir = getDirectory("openDirMenu", true); // NOI18N
169        if (dir != null) {
170            doPreviewDialog(dir, new MActionListener(dir, true),
171                    null, new CActionListener(), 0);
172            closeWaitFrame();
173        }
174    }
175
176    public void searchFS() {
177        clearSearch();
178        File dir = getDirectory("searchFSMenu", false); // NOI18N
179        showWaitFrame("searchWait", dir);
180        if (dir != null) {
181            _searcher = new Seacher(dir);
182            _searcher.start();
183        }
184    }
185
186    void searcherDone(File dir, int count) {
187        if (_previewDialog != null) {
188            _previewDialog.dispose();
189        }
190        closeWaitFrame();
191        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("numFound", count, dir.getAbsolutePath()),
192                Bundle.getMessage("MessageTitle"), JmriJOptionPane.INFORMATION_MESSAGE);
193    }
194
195    class Seacher extends Thread {
196
197        File dir;
198        boolean quit = false;
199        int count;
200
201        Seacher(File d) {
202            dir = d;
203        }
204
205        void quit() {
206            quit = true;
207        }
208
209        @Override
210        public void run() {
211            getImageDirectory(dir, CatalogTreeManager.IMAGE_FILTER);
212            if (log.isDebugEnabled()) {
213                log.debug("Searcher done for directory {}  quit={}", dir.getAbsolutePath(), quit);
214            }
215            ThreadingUtil.runOnGUI(() -> {
216                searcherDone(dir, count);
217            });
218        }
219
220        /**
221         * Find a Directory with image files.
222         * <p>
223         * This waits on completion of the PrivateDialong (which is itself not modal)
224         * so must not be called on the Layout or GUI threads
225         *
226         * @param dir    directory
227         * @param filter file filter for images
228         */
229        @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = {"WA_NOT_IN_LOOP", "UW_UNCOND_WAIT"}, justification="Waiting for single possible event")
230        private void getImageDirectory(File dir, String[] filter) {
231            if (jmri.util.ThreadingUtil.isGUIThread() || jmri.util.ThreadingUtil.isLayoutThread()) log.error("getImageDirectory called on wrong thread");
232
233            File[] files = dir.listFiles();
234            if (files == null || quit) {
235                // no sub directories
236                return;
237            }
238            int cnt = numImageFiles(dir);
239            if (log.isDebugEnabled()) {
240                log.debug("getImageDirectory dir= {} has {} files", dir.getAbsolutePath(), cnt);
241            }
242            count += cnt;
243            if (cnt > 0) {
244                ThreadingUtil.runOnGUI(() -> {
245                    doPreviewDialog(dir, new MActionListener(dir, false),
246                            new LActionListener(dir), new CActionListener(), 0);
247                });
248                // Since PreviewDialog is not modal, wait until user clicks a button to continue
249                synchronized (this) {
250                    try {
251                        wait();
252                    } catch (InterruptedException ie) {
253                        log.error("InterruptedException at _waitForSync", ie);
254                    } catch (java.lang.IllegalArgumentException iae) {
255                        log.error("Illegal argument getting Image Directory", iae);
256                    }
257                }
258            }
259            for (int k = 0; k < files.length; k++) {
260                if (files[k].isDirectory()) {
261                    if (quit) {
262                        return;
263                    }
264                    File f = files[k];
265                    ThreadingUtil.runOnGUI(() -> {
266                        showWaitFrame("searchWait", f);
267                    });
268//                    if (log.isDebugEnabled()) log.debug("getImageDirectory SubDir= {} of {} has {} files",
269//                            files[k].getName(), dir.getName(), numImageFiles(files[k]));
270                    getImageDirectory(files[k], filter);
271                }
272            }
273        }
274    }
275
276    // More action.  Directory dir has too many icons - display in separate windows
277    class MActionListener implements ActionListener {
278
279        File dir;
280        boolean oneDir;
281
282        public MActionListener(File d, boolean o) {
283            dir = d;
284            oneDir = o;
285        }
286
287        @Override
288        public void actionPerformed(ActionEvent a) {
289            displayMore(dir, oneDir);
290        }
291    }
292
293    // Continue looking for images
294    class LActionListener implements ActionListener {
295
296        File dir;
297
298        public LActionListener(File d) {
299            dir = d;
300        }
301
302        @Override
303        public void actionPerformed(ActionEvent a) {
304            keepLooking(dir);
305        }
306    }
307
308    // Cancel - Quit
309    class CActionListener implements ActionListener {
310
311        @Override
312        public void actionPerformed(ActionEvent a) {
313            cancelLooking();
314        }
315    }
316
317    private void doPreviewDialog(File dir, ActionListener moreAction,
318            ActionListener lookAction, ActionListener cancelAction, int startNum) {
319        showWaitFrame("previewWait", dir);
320        if (log.isDebugEnabled()) {
321            log.debug("doPreviewDialog dir= {}", dir.getAbsolutePath());
322        }
323
324        _previewDialog = new PreviewDialog(null, "previewDir", dir, CatalogTreeManager.IMAGE_FILTER);
325        _previewDialog.init(moreAction, lookAction, cancelAction, startNum);
326        _waitDialog.setVisible(false);
327    }
328
329    private void displayMore(File dir, boolean oneDir) {
330        if (log.isDebugEnabled()) {
331            log.debug("displayMore: dir= {} has {} files", dir.getName(), numImageFiles(dir));
332        }
333        if (_previewDialog != null) {
334            int numFilesShown = _previewDialog.getNumFilesShown();
335            ActionListener lookAction = _previewDialog.getLookActionListener();
336            _previewDialog.dispose();
337            if (numFilesShown > 0) {
338                doPreviewDialog(dir, new MActionListener(dir, oneDir),
339                        lookAction, new CActionListener(), numFilesShown);
340            }
341
342        } else {
343            synchronized (_searcher) {
344                _searcher.notify();
345            }
346        }
347    }
348
349    private void keepLooking(File dir) {
350        if (log.isDebugEnabled()) {
351            log.debug("keepLooking: dir= {} has {} files", dir.getName(), numImageFiles(dir));
352        }
353        if (_previewDialog != null) {
354            _previewDialog.dispose();
355            _previewDialog = null;
356        }
357        if (_searcher != null) {
358            synchronized (_searcher) {
359                _searcher.notify();
360            }
361        }
362    }
363
364    private void cancelLooking() {
365        closeWaitFrame();
366        if (_previewDialog != null) {
367            _previewDialog.dispose();
368            _previewDialog = null;
369        }
370        if (_searcher != null) {
371            synchronized (_searcher) {
372                _searcher.quit();
373                _searcher.notify();
374            }
375        }
376    }
377
378    public void close() {
379        closeWaitFrame();
380        cancelLooking();
381    }
382
383    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DirectorySearcher.class);
384}