Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: com/watsonnet/jcap/JCap.java


1   package com.watsonnet.jcap;
2   
3   // java:
4   import java.awt.*;
5   import java.awt.dnd.*;
6   import java.awt.datatransfer.*;
7   import java.awt.event.*;
8   import java.io.*;
9   import java.net.*;
10  import java.util.*;
11  import java.net.URL;
12  
13  // swing:
14  import javax.swing.*;
15  import javax.swing.event.*;
16  
17  // exif
18  import com.drew.metadata.*;
19  import com.drew.imaging.jpeg.*;
20  import com.drew.lang.*;
21  
22  // watsonnet
23  import com.watsonnet.jcap.*;
24  
25  /*
26  IDEAS:
27  
28  TODO:
29  
30  0.7
31  + Add JLabel("loading...") while image is loading
32  + Set native look and feel
33  + Set window icon
34  + Button to show full-size
35  + Sort image list by name
36  + Next without comment button
37  
38  0.7.1
39  + Move EXIF data into a JTable
40  + Add description and keyword text areas with easy keyboard navigation between them
41  + Clear exif, folder, image after going to an empty folder
42  + Add menu
43  + Change folder name to input box
44  + Fix annoying newline bug when loading/saving text data
45  
46  0.7.6
47  + Fixed bug where the "The JPEG image could not be decoded." message appeared
48    when viewing non-JPEG images
49  + Fixed bug with drag and drop where the dropped folder would not be remembered
50    if the app was closed after the folder was dropped.
51  + Code cleanup
52  
53  0.8
54  + Add search
55  + Button to apply current keywords to all images
56    (prompt, are you sure?).  No accelerator.
57  + Drag and drop (0.7.5)
58  - Handle java.lang.OutOfMemoryError when switching images too quickly
59  - Save position of toolbar
60  - A dialog that allows the user to rename all of the images in the current folder
61    based on the file modification date, EXIF picture date, or other data.  It should
62    also rename associated .txt files so that the categorization data is not lost.
63  
64  0.9
65  - Slideshow (full-screen with captions, etc.)
66  - Option panel (and options)
67  
68  1.0
69  - File list and/or thumbnail view and/or folder tree
70  */
71  
72  public class JCap extends JFrame implements DropTargetListener {
73    public static final String APP_TITLE = "JCap";
74    public static final String APP_VERSION = "0.7.6";
75    
76    private static final String INI_FILE = "jcap.ini";
77    
78    private static final int MODE_NEXT = 0;
79    private static final int MODE_PREV = 1;
80    private static final int MODE_BOTH = 2;
81    
82    public static Properties prop = new Properties();
83    public static File[] images;
84    public static int imageIndex = 0;
85    
86    // Search panel
87    private SearchPanel searchPanel = null;
88    public boolean searchPanelVisible = false;
89    
90    JPanel panelMain = new JPanel();
91    JPanel panelTop = new JPanel(new BorderLayout());
92    JPanel panelBottom = new JPanel(new BorderLayout());
93    
94    JToolBar toolbarMain = new JToolBar();
95    JButton btnChooser = new JButton();
96    JButton btnNext = new JButton();
97    JButton btnPrev = new JButton();
98    JButton btnExif = new JButton();
99    JButton btnNextEmpty = new JButton();
100   JButton btnZoom = new JButton();
101   JButton btnFolderInfo = new JButton();
102   JButton btnSearch = new JButton();
103   JButton btnApplyKeywords = new JButton();
104   
105   JPanel panelExif = new JPanel(new BorderLayout());
106   ExifTableModel exifData = new ExifTableModel();
107   JTable tableExif = new JTable(exifData);
108   JScrollPane scrollExif = new JScrollPane(tableExif);
109   
110   JPanel panelImageInfo = new JPanel(new BorderLayout());
111   ImagePanel panelImage = new ImagePanel();
112   JLabel labelFilename = new JLabel();
113   JTextField textPath = new JTextField();
114   
115   JPanel panelText = new JPanel(new BorderLayout());
116   JTextArea textDescription = new JTextArea();
117   JScrollPane scrollDescription = new JScrollPane(textDescription);
118   JTextField textCaption = new JTextField();
119   JTextField textKeywords = new JTextField();
120   
121   JSplitPane splitComment;
122   JSplitPane splitImageExif;
123   
124   JMenuBar menuBar = new JMenuBar();
125   
126   // constructor
127   public JCap() {
128     /*
129     +---------------------------+
130     | toolbar                   |
131     +-------------+-------------+
132     |             |             |
133     |   image     |    exif     |
134     |             |             |
135     |             |             |
136     +-------------+-------------+
137     | comment text area         |
138     |                           |
139     +---------------------------+
140     */
141     
142     // Load ini file
143     loadIni();
144     
145     // What a mess
146     
147     // ---------------------------------------------------------------------
148     // Main panel
149     // ---------------------------------------------------------------------
150     panelMain.setLayout(new BoxLayout(panelMain, BoxLayout.Y_AXIS));
151     setTitle(APP_TITLE + " " + APP_VERSION);
152     getContentPane().add(panelMain);
153     setIconImage(new ImageIcon(getClass().getResource("/images/icon.gif")).getImage());
154     // ---------------------------------------------------------------------
155     
156     // ---------------------------------------------------------------------
157     // Exif table
158     // ---------------------------------------------------------------------
159     scrollExif.setWheelScrollingEnabled(true);
160     panelExif.add(scrollExif, BorderLayout.CENTER);
161     // ---------------------------------------------------------------------
162     
163     // ---------------------------------------------------------------------
164     // Text areas
165     // ---------------------------------------------------------------------
166     textCaption.addActionListener(new ActionListener() {
167       public void actionPerformed(ActionEvent evt) {
168         textKeywords.setCaretPosition(0);
169         textKeywords.requestFocus();
170       }
171     });
172     
173     textKeywords.addActionListener(new ActionListener() {
174       public void actionPerformed(ActionEvent evt) {
175         textDescription.setCaretPosition(0);
176         textDescription.requestFocus();
177       }
178     });
179     
180     textPath.addActionListener(new ActionListener() {
181       public void actionPerformed(ActionEvent evt) {
182         openFolder(textPath.getText());
183       }
184     });
185     
186     textDescription.setLineWrap(true);
187     textDescription.setWrapStyleWord(true);
188     textDescription.setFont(Font.getFont("SansSerif"));
189     scrollDescription.setWheelScrollingEnabled(true);
190     scrollDescription.getVerticalScrollBar().setUnitIncrement(textDescription.getFontMetrics(textDescription.getFont()).getHeight());
191     scrollDescription.getHorizontalScrollBar().setUnitIncrement(textDescription.getFontMetrics(textDescription.getFont()).stringWidth("W"));
192     scrollDescription.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
193     
194     JPanel panelCaptionKeywordsLabels = new JPanel();
195     panelCaptionKeywordsLabels.setLayout(new GridLayout(0,1));
196     panelCaptionKeywordsLabels.add(new JLabel("Caption:", JLabel.RIGHT));
197     panelCaptionKeywordsLabels.add(new JLabel("Keywords:", JLabel.RIGHT));
198     
199     JPanel panelKeywordsAndButton = new JPanel(new BorderLayout());
200     panelKeywordsAndButton.add(textKeywords, BorderLayout.CENTER);
201     panelKeywordsAndButton.add(btnApplyKeywords, BorderLayout.EAST);
202     
203     JPanel panelCaptionKeywordsFields = new JPanel();
204     panelCaptionKeywordsFields.setLayout(new GridLayout(0,1));
205     panelCaptionKeywordsFields.add(textCaption);
206     panelCaptionKeywordsFields.add(panelKeywordsAndButton);
207     
208     JPanel panelCaptionKeywords = new JPanel(new BorderLayout());
209     panelCaptionKeywords.add(panelCaptionKeywordsLabels, BorderLayout.WEST);
210     panelCaptionKeywords.add(panelCaptionKeywordsFields, BorderLayout.CENTER);
211     
212     JPanel panelDescription = new JPanel(new BorderLayout());
213     panelDescription.setBorder(BorderFactory.createTitledBorder("Description"));
214     panelDescription.add(scrollDescription, BorderLayout.CENTER);
215     
216     panelText.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
217     panelText.add(panelCaptionKeywords, BorderLayout.NORTH);
218     panelText.add(panelDescription, BorderLayout.CENTER);
219     panelBottom.add(panelText, BorderLayout.CENTER);
220     //panelBottom.setBorder(BorderFactory.createEtchedBorder());
221     // ---------------------------------------------------------------------
222     
223     // ---------------------------------------------------------------------
224     // Menu
225     // ---------------------------------------------------------------------
226     JMenu menu, submenu;
227     JMenuItem menuItem;
228     setJMenuBar(menuBar);
229     
230     // File menu
231     menu = new JMenu("File");
232     menu.setMnemonic(KeyEvent.VK_F);
233     menuBar.add(menu);
234     
235       menuItem = new JMenuItem("Open folder...");
236       menuItem.setMnemonic(KeyEvent.VK_O);
237       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
238       menuItem.addActionListener(new ActionListener() {
239         public void actionPerformed(ActionEvent evt) {
240           chooseFolder();
241         }
242       });
243       menu.add(menuItem);
244       
245       menu.addSeparator();
246       
247       menuItem = new JMenuItem("Quit");
248       menuItem.setMnemonic(KeyEvent.VK_Q);
249       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
250       menuItem.addActionListener(new ActionListener() {
251         public void actionPerformed(ActionEvent evt) {
252           closeApplication();
253         }
254       });
255       menu.add(menuItem);
256     
257     // Image menu
258     menu = new JMenu("Image");
259     menu.setMnemonic(KeyEvent.VK_I);
260     menuBar.add(menu);
261     
262       menuItem = new JMenuItem("View previous image");
263       menuItem.setMnemonic(KeyEvent.VK_P);
264       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.CTRL_MASK));
265       menuItem.addActionListener(new ActionListener() {
266         public void actionPerformed(ActionEvent evt) {
267           getPreviousImage();
268         }
269       });
270       menu.add(menuItem);
271       
272       menuItem = new JMenuItem("View next image");
273       menuItem.setMnemonic(KeyEvent.VK_N);
274       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
275       menuItem.addActionListener(new ActionListener() {
276         public void actionPerformed(ActionEvent evt) {
277           getNextImage();
278         }
279       });
280       menu.add(menuItem);
281       
282       menuItem = new JMenuItem("View next image without a caption");
283       menuItem.setMnemonic(KeyEvent.VK_ENTER);
284       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, ActionEvent.CTRL_MASK));
285       menuItem.addActionListener(new ActionListener() {
286         public void actionPerformed(ActionEvent evt) {
287           getNextImageNoComment();
288         }
289       });
290       menu.add(menuItem);
291       
292       menuItem = new JMenuItem("Show image full size");
293       menuItem.setMnemonic(KeyEvent.VK_Z);
294       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
295       menuItem.addActionListener(new ActionListener() {
296         public void actionPerformed(ActionEvent evt) {
297           showFullSize(imageIndex);
298         }
299       });
300       menu.add(menuItem);
301       
302       menuItem = new JMenuItem("Append EXIF data to comment");
303       menuItem.setMnemonic(KeyEvent.VK_X);
304       menuItem.addActionListener(new ActionListener() {
305         public void actionPerformed(ActionEvent evt) {
306           copyExifToComment(imageIndex);
307         }
308       });
309       menu.add(menuItem);
310       
311       menuItem = new JMenuItem("Update folder information");
312       menuItem.setMnemonic(KeyEvent.VK_F);
313       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, ActionEvent.CTRL_MASK));
314       menuItem.addActionListener(new ActionListener() {
315         public void actionPerformed(ActionEvent evt) {
316           updateFolderInfo();
317         }
318       });
319       menu.add(menuItem);
320       
321     // Search menu
322     menu = new JMenu("Search");
323     menu.setMnemonic(KeyEvent.VK_S);
324     menuBar.add(menu);
325     
326       menuItem = new JMenuItem("Search for pictures");
327       menuItem.setMnemonic(KeyEvent.VK_S);
328       menuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
329       menuItem.addActionListener(new ActionListener() {
330         public void actionPerformed(ActionEvent evt) {
331           showSearchDialog();
332         }
333       });
334       menu.add(menuItem);
335     // ---------------------------------------------------------------------
336       
337     // ---------------------------------------------------------------------
338     // Toolbar
339     // ---------------------------------------------------------------------
340     panelTop.add(toolbarMain, BorderLayout.NORTH);
341     
342     // Folder button
343     btnChooser.setToolTipText("Choose a folder to browse");
344     btnChooser.setIcon(new ImageIcon(getClass().getResource("/images/open.gif")));
345     btnChooser.addActionListener(new ActionListener() {
346       public void actionPerformed(ActionEvent evt) {
347         chooseFolder();
348       }
349     });
350     toolbarMain.add(btnChooser);
351     
352     // Previous button
353     btnPrev.setToolTipText("View previous image");
354     btnPrev.setIcon(new ImageIcon(getClass().getResource("/images/previous.gif")));
355     btnPrev.addActionListener(new ActionListener() {
356       public void actionPerformed(ActionEvent evt) {
357         getPreviousImage();
358       }
359     });
360     toolbarMain.add(btnPrev);
361     
362     // Next button
363     btnNext.setToolTipText("View next image");
364     btnNext.setIcon(new ImageIcon(getClass().getResource("/images/next.gif")));
365     btnNext.addActionListener(new ActionListener() {
366       public void actionPerformed(ActionEvent evt) {
367         getNextImage();
368       }
369     });
370     toolbarMain.add(btnNext);
371     
372     // Next without text button
373     btnNextEmpty.setToolTipText("View next image without a caption");
374     btnNextEmpty.setIcon(new ImageIcon(getClass().getResource("/images/next_empty.gif")));
375     btnNextEmpty.addActionListener(new ActionListener() {
376       public void actionPerformed(ActionEvent evt) {
377         getNextImageNoComment();
378       }
379     });
380     toolbarMain.add(btnNextEmpty);
381     
382     // Zoom into image
383     btnZoom.setToolTipText("Show image full size");
384     btnZoom.setIcon(new ImageIcon(getClass().getResource("/images/zoom.gif")));
385     btnZoom.addActionListener(new ActionListener() {
386       public void actionPerformed(ActionEvent evt) {
387         showFullSize(imageIndex);
388       }
389     });
390     toolbarMain.add(btnZoom);
391     
392     // Append EXIF button
393     btnExif.setToolTipText("Append EXIF data to comment");
394     btnExif.setIcon(new ImageIcon(getClass().getResource("/images/appendexif.gif")));
395     btnExif.addActionListener(new ActionListener() {
396       public void actionPerformed(ActionEvent evt) {
397         copyExifToComment(imageIndex);
398       }
399     });
400     toolbarMain.add(btnExif);
401     
402     // Folder information button
403     btnFolderInfo.setToolTipText("Update folder information");
404     btnFolderInfo.setIcon(new ImageIcon(getClass().getResource("/images/folderinfo.gif")));
405     btnFolderInfo.addActionListener(new ActionListener() {
406       public void actionPerformed(ActionEvent evt) {
407         updateFolderInfo();
408       }
409     });
410     toolbarMain.add(btnFolderInfo);
411     
412     // Search
413     btnSearch.setToolTipText("Search");
414     btnSearch.setIcon(new ImageIcon(getClass().getResource("/images/find.gif")));
415     btnSearch.addActionListener(new ActionListener() {
416       public void actionPerformed(ActionEvent evt) {
417         showSearchDialog();
418       }
419     });
420     toolbarMain.add(btnSearch);
421     // ---------------------------------------------------------------------
422     
423     // ---------------------------------------------------------------------
424     // Other buttons
425     // ---------------------------------------------------------------------
426     // Search
427     btnApplyKeywords.setToolTipText("Apply to all");
428     btnApplyKeywords.setIcon(new ImageIcon(getClass().getResource("/images/saveall.gif")));
429     btnApplyKeywords.addActionListener(new ActionListener() {
430       public void actionPerformed(ActionEvent evt) {
431         applyKeywordsToAll();
432       }
433     });
434     
435     // ---------------------------------------------------------------------
436     // Create split panes
437     // ---------------------------------------------------------------------
438     splitComment = new JSplitPane(JSplitPane.VERTICAL_SPLIT, panelTop, panelBottom);
439     splitComment.setOneTouchExpandable(true);
440     splitComment.setContinuousLayout(true);
441     splitComment.setDividerSize(8);
442     splitImageExif = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panelImage, panelExif);
443     splitImageExif.setOneTouchExpandable(true);
444     splitImageExif.setContinuousLayout(true);
445     splitImageExif.setDividerSize(8);
446     // ---------------------------------------------------------------------
447     
448     // ---------------------------------------------------------------------
449     // Image panel
450     // ---------------------------------------------------------------------
451     //panelImage.setBorder(BorderFactory.createEtchedBorder());
452     panelImageInfo.add(splitImageExif, BorderLayout.CENTER);
453     JPanel panelPath = new JPanel(new BorderLayout());
454     panelPath.add(labelFilename, BorderLayout.WEST);
455     panelPath.add(textPath, BorderLayout.CENTER);
456     panelImageInfo.add(panelPath, BorderLayout.NORTH);
457     labelFilename.setBorder(BorderFactory.createEtchedBorder());
458     panelTop.add(panelImageInfo, BorderLayout.CENTER);
459     // ---------------------------------------------------------------------
460     
461     // ---------------------------------------------------------------------
462     // Add panels
463     // ---------------------------------------------------------------------
464     panelMain.add(splitComment, BorderLayout.CENTER);
465     // ---------------------------------------------------------------------
466     
467     // ---------------------------------------------------------------------
468     // Enable drag and drop
469     // ---------------------------------------------------------------------
470     this.setDropTarget(new DropTarget(this, this));
471     // ---------------------------------------------------------------------
472     
473     // ---------------------------------------------------------------------
474     // Validate
475     // ---------------------------------------------------------------------
476     panelMain.validate();
477     // ---------------------------------------------------------------------
478     
479     // ---------------------------------------------------------------------
480     // Add window listener to trap closing event
481     // ---------------------------------------------------------------------
482     addWindowListener(new WindowListener() {
483       public void windowClosing(WindowEvent e) {
484         closeApplication();
485       }
486       public void windowClosed(WindowEvent e) {}
487       public void windowOpened(WindowEvent e) {}
488       public void windowIconified(WindowEvent e) {}
489       public void windowDeiconified(WindowEvent e) {}
490       public void windowActivated(WindowEvent e) {}
491       public void windowDeactivated(WindowEvent e) {}
492     });
493     // ---------------------------------------------------------------------
494     
495     // ---------------------------------------------------------------------
496     // Load window position
497     // ---------------------------------------------------------------------
498     try {
499       int x = Integer.parseInt(prop.getProperty("WindowX"));
500       int y = Integer.parseInt(prop.getProperty("WindowY"));
501       int w = Integer.parseInt(prop.getProperty("WindowW"));
502       int h = Integer.parseInt(prop.getProperty("WindowH"));
503       setBounds(x, y, w, h);
504     } catch (NumberFormatException ex) {
505       setBounds(0, 0, 600, 450);
506     }
507     // ---------------------------------------------------------------------
508     
509     // ---------------------------------------------------------------------
510     // Load split locations
511     // ---------------------------------------------------------------------
512     try {
513       int p1 = Integer.parseInt(prop.getProperty("Split1"));
514       int p2 = Integer.parseInt(prop.getProperty("Split2"));
515       splitImageExif.setDividerLocation(p1);
516       splitComment.setDividerLocation(p2);
517     } catch (NumberFormatException ex) {
518       splitImageExif.setDividerLocation(400);
519       splitComment.setDividerLocation(300);
520     }
521     // ---------------------------------------------------------------------
522     
523     // ---------------------------------------------------------------------
524     // Make frame visible
525     // ---------------------------------------------------------------------
526     setVisible(true);
527     // ---------------------------------------------------------------------
528     
529     // Open images in last folder opened
530     showFirstImage();
531   }
532   
533   // Search
534   private void showSearchDialog() {
535     if (!searchPanelVisible) {
536       searchPanel = new SearchPanel();
537       searchPanel.parent = this;
538       searchPanel.setBounds(getBounds().x + getBounds().width/5, getBounds().y + getBounds().height/5, getBounds().width*3/5, getBounds().height*3/5);
539       searchPanel.validate();
540       
541       // So we only create one search panel.
542       searchPanelVisible = true;
543     } else {
544       searchPanel.requestFocus();
545     }
546   }
547   
548   // get images and show first one
549   private void showFirstImage() {
550     if (getLastFolder() == null) { return; }
551     getImages(getLastFolder());
552     if (images == null || images.length <= 0) { return; }
553     imageIndex = 0;
554     showImage(Toolkit.getDefaultToolkit().getImage(images[imageIndex].getAbsolutePath()));
555   }
556   
557   // show the image at the current index
558   public void showImage(Image img, boolean focus) {
559     if (images != null && images.length > 0) {
560       panelImage.setImage(null);
561       panelImage.setImage(img);
562       panelImage.repaint();
563       loadText(imageIndex, focus);
564     }
565   }
566   
567   public void showImage(Image img) {
568     showImage(img, true);
569   }
570   
571   // Show the image full size
572   private void showFullSize(int n) {
573     ImagePanel ip = new ImagePanel();
574     ip.setImage(Toolkit.getDefaultToolkit().getImage(images[n].getAbsolutePath()));
575     
576     JFrame f = new JFrame(APP_TITLE + " " + APP_VERSION + " " + images[n].getName());
577     f.setIconImage(new ImageIcon(getClass().getResource("/images/icon.gif")).getImage());
578     f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
579     //f.setBounds(getBounds().x, getBounds().y, getBounds().width, getBounds().height); // Set frame to same size and position as main window
580     f.setSize(getSize()); // Set frame to same size as main window
581     f.getContentPane().setLayout(new BorderLayout());
582     f.getContentPane().add(ip, BorderLayout.CENTER);
583     f.setVisible(true);
584     f.setExtendedState(JFrame.MAXIMIZED_BOTH); // maximize
585   }
586   
587   // show the previous image
588   private void getPreviousImage() {
589     if (images == null || images.length <= 0) { return; }
590     
591     // update text
592     saveText(imageIndex);
593     
594     // update image pointer
595     imageIndex -= 1;
596     if (imageIndex < 0) { imageIndex = images.length-1; }
597     
598     // show image
599     showImage(Toolkit.getDefaultToolkit().getImage(images[imageIndex].getAbsolutePath()));
600   }
601   
602   // show the next image
603   private void getNextImage() {
604     if (images == null || images.length <= 0) { return; }
605     
606     // update text
607     saveText(imageIndex);
608     
609     // update image pointer
610     imageIndex += 1;
611     if (imageIndex > images.length-1) { imageIndex = 0; }
612     
613     // show image
614     showImage(Toolkit.getDefaultToolkit().getImage(images[imageIndex].getAbsolutePath()));
615   }
616   
617   // show the next image without a caption
618   private void getNextImageNoComment() {
619     if (images == null || images.length <= 0) { return; }
620     
621     // update text
622     saveText(imageIndex);
623     
624     // find next image without comments
625     int i = imageIndex+1;
626     if (i > images.length-1) { i = 0; }
627     int start = imageIndex;
628     boolean found = false;
629     while (true) {
630       File f = new File(getCaptionFileName(i));
631       if (!f.exists() || f.length() <= 0) {
632         imageIndex = i;
633         found = true;
634         break;
635       }
636       if (i == start) { break; }
637       i += 1;
638       if (i > images.length-1) { i = 0; }
639     }
640     
641     if (found) {
642       // show image
643       showImage(Toolkit.getDefaultToolkit().getImage(images[imageIndex].getAbsolutePath()));
644     } else {
645       // message box, everything is commented
646       JOptionPane.showConfirmDialog(this, "All images in this folder are captioned.", "Not found", JOptionPane.OK_CANCEL_OPTION);
647     }
648   }
649   
650   // Apply current keywords to all images in this folder after confirmation
651   private void applyKeywordsToAll() {
652     if (images == null || images.length <= 0) { return; }
653     
654     if (JOptionPane.showConfirmDialog(this, "All existing keywords will be erased.\nAre you sure you want to apply these keywords\nto all images in this folder?") == JOptionPane.YES_OPTION) {
655       String keywords = textKeywords.getText();
656       for(int i=0;i<=images.length-1;i++) {
657         saveFile(new File(getKeywordsFileName(i)), keywords);
658       }
659       JOptionPane.showConfirmDialog(this, "Keywords applied to all images.", "Done", JOptionPane.OK_CANCEL_OPTION);
660     }
661   }
662   
663   // get the filename of the txt file associated with each image
664   private String getTextFileName(int n) {
665     return(Util.getTextFileName(images[n].getAbsolutePath()));
666   }
667   
668   // get the filename of the txt file associated with each image
669   private String getKeywordsFileName(int n) {
670     return(Util.getKeywordsFileName(images[n].getAbsolutePath()));
671   }
672   
673   // get the filename of the txt file associated with each image
674   private String getCaptionFileName(int n) {
675     return(Util.getCaptionFileName(images[n].getAbsolutePath()));
676   }
677   
678   // Loads the txt file associated with the image and displays the text
679   // in the textArea box.
680   private void loadText(int n, boolean focus) {
681     // Clear existing text
682     clearText();
683     
684     // Get descriptions
685     textDescription.setText(Util.readFile(new File(getTextFileName(n))));
686     textCaption.setText(Util.readFile(new File(getCaptionFileName(n))));
687     textKeywords.setText(Util.readFile(new File(getKeywordsFileName(n))));
688     
689     // Move caret to top and get focus
690     if (focus) {
691       textCaption.setCaretPosition(0);
692       textCaption.requestFocus();
693     }
694     
695     // Get EXIF data
696     addExifData(exifData, n);
697     
698     // Update path
699     textPath.setText(images[n].getParent());
700     textPath.setCaretPosition(textPath.getText().length()-1);
701     
702     // File index and max number of images
703     String index = (new Integer(n+1)).toString() + "/" + (new Integer(images.length)).toString();
704     
705     // Update label
706     labelFilename.setText(Util.getFilenameLabel(images[n].getName()) + " (" + index + ")");
707   }
708   
709   private void loadText(int n) {
710     loadText(n, true);
711   }
712   
713   // Clear the text areas
714   private void clearText() {
715     textDescription.setText("");
716     textCaption.setText("");
717     textKeywords.setText("");
718     labelFilename.setText("");
719     exifData.clearData();
720   }
721   
722   // Save the given data to the given file
723   private void saveFile(File f, String data) {
724     // Don't create txt files if the DeleteEmpty option is on and there is no comment
725     if (prop.getProperty("DeleteEmpty").equalsIgnoreCase("1") && data.length() <= 0) {
726       if (f.exists()) { f.delete(); }
727       return;
728     }
729     
730     Util.saveFile(f, data);
731   }
732   
733   // Save text of the current picture to <picture name>.txt
734   private void saveText(int n) {
735     saveFile(new File(getTextFileName(n)), textDescription.getText());
736     saveFile(new File(getCaptionFileName(n)), textCaption.getText());
737     saveFile(new File(getKeywordsFileName(n)), textKeywords.getText());
738   }
739   
740   // appends the exif data to the comment textarea
741   private void copyExifToComment(int n) {
742     if (images == null || images.length <= 0) { return; }
743     
744     StringBuffer sb = new StringBuffer();
745     
746     // get current text
747     sb.append(textDescription.getText());
748     sb.append("\n");
749     
750     // get exif
751     sb.append(getExifData(n));
752     
753     // set text
754     textDescription.setText(sb.toString());
755   }
756   
757   // gets the exif data and returns it as a string
758   private String getExifData(int n) {
759     if (images == null || images.length <= 0) { return(""); }
760     if (!Util.supportsEXIF(images[n].getAbsolutePath())) { return(""); }
761     
762     try {
763       StringBuffer sb = new StringBuffer();
764       Metadata metadata = JpegMetadataReader.readMetadata(images[n]);
765       
766       // iterate through metadata directories
767       Iterator directories = metadata.getDirectoryIterator();
768       while (directories.hasNext()) {
769         Directory directory = (Directory)directories.next();
770         
771         // iterate through tags
772         Iterator tags = directory.getTagIterator();
773         while (tags.hasNext()) {
774           Tag tag = (Tag)tags.next();
775           
776           // append each tag to stringbuffer
777           if (sb.length() > 0) { sb.append("\n"); }
778           sb.append(tag.toString());
779         }
780       }
781       
782       // return text
783       return(sb.toString());
784     } catch (java.io.FileNotFoundException ex) {
785       JOptionPane.showMessageDialog(this, "The image could not be found\nwhile searching for EXIF data.", "File not found", JOptionPane.WARNING_MESSAGE);
786       return("");
787     } catch (com.drew.imaging.jpeg.JpegProcessingException ex) {
788       JOptionPane.showMessageDialog(this, "The JPEG image could not be decoded.", "JPEG error", JOptionPane.WARNING_MESSAGE);
789       return("");
790     }
791   }
792   
793   // Add exif data to table mode
794   private void addExifData(ExifTableModel m, int n) {
795     if (images == null || images.length <= 0) { return; }
796     if (!Util.supportsEXIF(images[n].getAbsolutePath())) { return; }
797     
798     try {
799       Metadata metadata = JpegMetadataReader.readMetadata(images[n]);
800       
801       // iterate through metadata directories
802       Iterator directories = metadata.getDirectoryIterator();
803       while (directories.hasNext()) {
804         Directory directory = (Directory)directories.next();
805         
806         // iterate through tags
807         Iterator tags = directory.getTagIterator();
808         while (tags.hasNext()) {
809           Tag tag = (Tag)tags.next();
810           
811           // append each tag to stringbuffer
812           try {
813             m.addData(tag.getTagName(), tag.getDescription());
814           } catch (com.drew.metadata.MetadataException ex) {
815             JOptionPane.showMessageDialog(this, "The EXIF data could not be added to the table.", "Error", JOptionPane.WARNING_MESSAGE);
816           }
817         }
818       }
819     } catch (java.io.FileNotFoundException ex) {
820       JOptionPane.showMessageDialog(this, "The image could not be found\nwhile searching for EXIF data.", "File not found", JOptionPane.WARNING_MESSAGE);
821     } catch (com.drew.imaging.jpeg.JpegProcessingException ex) {
822       JOptionPane.showMessageDialog(this, "The JPEG image could not be decoded.", "JPEG error", JOptionPane.WARNING_MESSAGE);
823     }
824   }
825   
826   // choose a folder
827   private void chooseFolder() {
828     // folder chooser
829     JFileChooser fc;
830     if (images != null && images.length > 0) {
831       fc = new JFileChooser(images[imageIndex]);
832     } else {
833       if (getLastFolder() != null) {
834         fc = new JFileChooser(getLastFolder());
835       } else {
836         fc = new JFileChooser();
837       }
838     }
839     fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
840     
841     if (fc.showOpenDialog(panelMain) == JFileChooser.APPROVE_OPTION) {
842       openFolder(fc.getSelectedFile().getAbsolutePath());
843     }
844   }
845   
846   // open the chosen folder and start showing images
847   private void openFolder(String folder) {
848     // save last folder
849     setLastFolder(folder);
850     
851     // get images and show first one
852     showFirstImage();
853   }
854   
855   // save last folder
856   private void setLastFolder(String folder) {
857     prop.setProperty("LastFolder", folder);
858   }
859   
860   // get last folder
861   private String getLastFolder() {
862     return(prop.getProperty("LastFolder"));
863   }
864   
865   // update folder information
866   private void updateFolderInfo() {
867     if (images == null || images.length <= 0) { return; }
868     
869     // Get path for title
870     String path = images[0].getParent();
871     if (path.length() > 40) {
872       // find closest File.separator
873       int p = path.indexOf(File.separator, path.length()-40);
874       if (p < 0) { p = 0; }
875       path = "..." + path.substring(p, path.length());
876     }
877     
878     // Frame (defined below)
879     // Changed to JDialog so I could get modal (was JFrame)
880     final JDialog myframe = new JDialog(this, APP_TITLE + " " + APP_VERSION + " " + path, true);
881     
882     // Text area
883     final JTextArea textInfo = new JTextArea();
884     JScrollPane scrollInfo = new JScrollPane(textInfo);
885     
886     textInfo.setLineWrap(true);
887     textInfo.setWrapStyleWord(true);
888     textInfo.setEnabled(true);
889     textInfo.setFont(Font.getFont("SansSerif"));
890     scrollInfo.setWheelScrollingEnabled(true);
891     scrollInfo.getVerticalScrollBar().setUnitIncrement(textInfo.getFontMetrics(textInfo.getFont()).getHeight());
892     scrollInfo.getHorizontalScrollBar().setUnitIncrement(textInfo.getFontMetrics(textInfo.getFont()).stringWidth("W"));
893     
894     JPanel panelText = new JPanel(new BorderLayout());
895     panelText.setBorder(BorderFactory.createTitledBorder("Description"));
896     panelText.add(scrollInfo, BorderLayout.CENTER);
897     
898     // Ok and Cancel buttons
899     JButton btnOK = new JButton("Ok");
900     btnOK.addActionListener(new ActionListener() {
901       public void actionPerformed(ActionEvent evt) {
902         // Save text and close window
903         try {
904           File f = new File(images[0].getParent(), Util.FOLDERINFO_FILENAME);
905           f.createNewFile();
906           if (f.canWrite()) {
907             FileWriter fw = new FileWriter(f);
908             fw.write(textInfo.getText());
909             fw.flush();
910             fw.close();
911           }
912         } catch (java.io.IOException ex) {
913           JOptionPane.showMessageDialog(myframe, "Could not save folder information.", "Save error", JOptionPane.WARNING_MESSAGE);
914         }
915         myframe.dispose();
916       }
917     });
918     JButton btnCancel = new JButton("Cancel");
919     btnCancel.addActionListener(new ActionListener() {
920       public void actionPerformed(ActionEvent evt) {
921         // just close window
922         myframe.dispose();
923       }
924     });
925     JPanel panelButtons = new JPanel(new FlowLayout());
926     panelButtons.add(btnOK);
927     panelButtons.add(btnCancel);
928     
929     // Load text
930     try {
931       File f = new File(images[0].getParent(), Util.FOLDERINFO_FILENAME);
932       StringBuffer sb = new StringBuffer();
933       if (f.canRead()) {
934         String line;
935         BufferedReader br = new BufferedReader(new FileReader(f));
936         while ((line = br.readLine()) != null) {
937           if (sb.length() > 0) { sb.append("\n"); }
938           sb.append(line);
939         }
940       }
941       textInfo.setText(sb.toString());
942       
943       // Move caret to top and get focus
944       textCaption.setCaretPosition(0);
945       textCaption.requestFocus();
946     } catch (java.io.IOException ex) {
947       JOptionPane.showMessageDialog(this, "Could not read folder information.", "Read error", JOptionPane.WARNING_MESSAGE);
948     }
949     
950     // New frame
951     //myframe.setIconImage(new ImageIcon(getClass().getResource("/images/icon.gif")).getImage()); // method of JFrame
952     myframe.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // same as cancel
953     // set frame to same size and position as main window
954     myframe.setBounds(getBounds().x + getBounds().width/5, getBounds().y + getBounds().height/5, getBounds().width*3/5, getBounds().height*3/5);
955     // set frame to same size as main window
956     //f.setSize(getSize());
957     myframe.getContentPane().setLayout(new BorderLayout());
958     myframe.getContentPane().add(panelText, BorderLayout.CENTER);
959     myframe.getContentPane().add(panelButtons, BorderLayout.SOUTH);
960     myframe.setVisible(true);
961   }
962   
963   // Get a File[] of images in the given folder
964   public void getImages(String folder) {
965     File f = new File(folder);
966     images = f.listFiles(new FileFilter() {
967       public boolean accept(File f) {
968         // Only include certain types in the image list
969         if (f != null) {
970           return(Util.isImageType(f.getAbsolutePath()));
971         }
972         return(false);
973       }
974     });
975     
976     // sort file list
977     if (images != null && images.length > 0) {
978       Arrays.sort(images);
979     }
980   }
981   
982   // Get the user specific ini filename based on INI_FILE
983   private String getIniFilename() {
984     String ini = System.getProperty("user.home") + File.separator + "JCap";
985     File f = new File(ini);
986     if (!f.exists()) { f.mkdirs(); }
987     return(ini + File.separator + INI_FILE);
988   }
989   
990   // load settings
991   private void loadIni() {
992     try {
993       File f = new File(getIniFilename());
994       if (f.exists()) {
995         prop.load(new FileInputStream(getIniFilename()));
996       } else {
997         f.createNewFile();
998       }
999       
1000      // Defaults
1001      if (prop.getProperty("DeleteEmpty") == null) {
1002        prop.setProperty("DeleteEmpty", "1");
1003      }
1004    } catch (FileNotFoundException ex) {
1005      JOptionPane.showMessageDialog(this, "Settings file not found.", "Read error", JOptionPane.WARNING_MESSAGE);
1006    } catch (IOException ex) {
1007      JOptionPane.showMessageDialog(this, "Could not read settings.", "Read error", JOptionPane.WARNING_MESSAGE);
1008    }
1009  }
1010  
1011  // save settings
1012  public void saveIni() {
1013    try {
1014      // Window size and position
1015      Rectangle r = getBounds();
1016      prop.setProperty("WindowX", (new Integer(r.x)).toString());
1017      prop.setProperty("WindowY", (new Integer(r.y)).toString());
1018      prop.setProperty("WindowW", (new Integer(r.width)).toString());
1019      prop.setProperty("WindowH", (new Integer(r.height)).toString());
1020      
1021      // Splitter positions
1022      prop.setProperty("Split1", (new Integer(splitImageExif.getDividerLocation())).toString());
1023      prop.setProperty("Split2", (new Integer(splitComment.getDividerLocation())).toString());
1024      
1025      // Save settings
1026      prop.store(new FileOutputStream(getIniFilename()), "JCap settings");
1027    } catch (FileNotFoundException ex) {
1028      JOptionPane.showMessageDialog(this, "Settings file not found.", "Save error", JOptionPane.WARNING_MESSAGE);
1029    } catch (IOException ex) {
1030      JOptionPane.showMessageDialog(this, "Could not save settings.", "Save error", JOptionPane.WARNING_MESSAGE);
1031    }
1032  }
1033  
1034  // quit
1035  private void closeApplication() {
1036    if (images != null && images.length > 0) {
1037      // save text currently being worked on
1038      saveText(imageIndex);
1039    }
1040    saveIni();
1041    System.exit(0);
1042  }
1043  
1044  // -------------------------------------------------------------------------
1045  // DropTargetListener interface
1046  // -------------------------------------------------------------------------
1047  // Called while a drag operation is ongoing, when the mouse pointer enters
1048  // the operable part of the drop site for the DropTarget registered with
1049  // this listener.
1050  public void dragEnter(DropTargetDragEvent dtde) {
1051  }
1052  // Called while a drag operation is ongoing, when the mouse pointer has
1053  // exited the operable part of the drop site for the DropTarget registered
1054  // with this listener.
1055  public void dragExit(DropTargetEvent dte) {
1056  }
1057  // Called when a drag operation is ongoing, while the mouse pointer is still
1058  // over the operable part of the drop site for the DropTarget registered
1059  // with this listener.
1060  public void dragOver(DropTargetDragEvent dtde) {
1061  }
1062  // Called when the drag operation has terminated with a drop on the operable
1063  // part of the drop site for the DropTarget registered with this listener.
1064  public void drop(DropTargetDropEvent dtde) {
1065    if ((dtde.getSourceActions() & DnDConstants.ACTION_COPY) != 0) {
1066      dtde.acceptDrop(DnDConstants.ACTION_COPY);
1067    } else {
1068      dtde.rejectDrop();
1069      return;
1070    }
1071    
1072    // Get the file list and attempt to open the folder that the first file
1073    // is in.
1074    try {
1075      java.util.List files = (java.util.List)dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
1076      dtde.dropComplete(true);
1077      File f = (File)files.get(0);
1078      if (f.exists()) {
1079        if (f.isFile()) {
1080          setLastFolder(f.getParent());
1081          getImages(f.getParent());
1082          for(int i = 0; i <= images.length-1; i++) {
1083            if (Util.getFilenameLabel(images[i].getAbsolutePath()).equals(Util.getFilenameLabel(f.getAbsolutePath()))) {
1084              imageIndex = i;
1085              break;
1086            }
1087          }
1088          showImage(Toolkit.getDefaultToolkit().getImage(images[imageIndex].getAbsolutePath()), false);
1089        } else {
1090          openFolder(f.getAbsolutePath());
1091        }
1092      } else {
1093        JOptionPane.showMessageDialog(this, "Drag a file or folder to open it.", "Can't open file", JOptionPane.WARNING_MESSAGE);
1094      }
1095      return;
1096    } catch (java.io.IOException ex) {
1097      JOptionPane.showMessageDialog(this, "Unknown IO error during drag and drop.", "Drag and drop error", JOptionPane.WARNING_MESSAGE);
1098    } catch (UnsupportedFlavorException ex) {
1099      JOptionPane.showMessageDialog(this, "The data you tried to drop here is not supported.", "Drag and drop error", JOptionPane.WARNING_MESSAGE);
1100    }
1101    dtde.dropComplete(false);
1102  }
1103  // Called if the user has modified the current drop gesture.
1104  public void dropActionChanged(DropTargetDragEvent dtde) {
1105  }
1106  // -------------------------------------------------------------------------
1107  
1108  // main
1109  public static void main(String[] args) {
1110    // Set native look and feel
1111    // Get the native look and feel class name
1112    String nativeLF = UIManager.getSystemLookAndFeelClassName();
1113    
1114    // Install the look and feel
1115    try {
1116      UIManager.setLookAndFeel(nativeLF);
1117    } catch (InstantiationException e) {
1118    } catch (ClassNotFoundException e) {
1119    } catch (UnsupportedLookAndFeelException e) {
1120    } catch (IllegalAccessException e) {
1121    }
1122    
1123    // Start JCap
1124    JCap application = new JCap();
1125  }
1126}
1127