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