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}