1 /* Copyright (C) 2005-2011 Fabio Riccardi */
2 
3 package com.lightcrafts.app;
4 
5 import static com.lightcrafts.app.Locale.LOCALE;
6 import com.lightcrafts.app.advice.AdviceManager;
7 import com.lightcrafts.app.menu.ComboFrameMenuBar;
8 import com.lightcrafts.app.menu.WindowMenu;
9 import com.lightcrafts.app.other.OtherApplication;
10 import com.lightcrafts.image.ImageInfo;
11 import com.lightcrafts.image.metadata.ImageMetadata;
12 import com.lightcrafts.model.Scale;
13 import com.lightcrafts.platform.AlertDialog;
14 import com.lightcrafts.platform.Platform;
15 import com.lightcrafts.platform.ProgressDialog;
16 import com.lightcrafts.templates.TemplateDatabase;
17 import com.lightcrafts.templates.TemplateKey;
18 import com.lightcrafts.ui.LightZoneSkin;
19 import com.lightcrafts.ui.MemoryMeter;
20 import com.lightcrafts.ui.action.ToggleAction;
21 import com.lightcrafts.ui.browser.ctrls.FolderCtrl;
22 import com.lightcrafts.ui.browser.folders.FolderBrowserPane;
23 import com.lightcrafts.ui.browser.folders.FolderTreeListener;
24 import com.lightcrafts.ui.browser.model.ImageDatum;
25 import com.lightcrafts.ui.browser.model.ImageDatumComparator;
26 import com.lightcrafts.ui.browser.model.ImageList;
27 import com.lightcrafts.ui.browser.model.PreviewUpdater;
28 import com.lightcrafts.ui.browser.view.*;
29 import com.lightcrafts.ui.editor.*;
30 import com.lightcrafts.ui.editor.assoc.DocumentDatabase;
31 import com.lightcrafts.ui.editor.assoc.DocumentDatabaseListener;
32 import com.lightcrafts.ui.export.SaveOptions;
33 import com.lightcrafts.ui.metadata2.MetadataScroll;
34 import com.lightcrafts.ui.templates.TemplateControl;
35 import com.lightcrafts.ui.templates.TemplateControlListener;
36 import com.lightcrafts.ui.toolkit.UICompliance;
37 import com.lightcrafts.utils.Version;
38 import com.lightcrafts.utils.filecache.FileCacheFactory;
39 import com.lightcrafts.utils.thread.ProgressThread;
40 
41 import javax.swing.*;
42 import javax.swing.border.Border;
43 import javax.swing.filechooser.FileSystemView;
44 import java.awt.*;
45 import java.awt.dnd.DnDConstants;
46 import java.awt.event.*;
47 import java.awt.image.RenderedImage;
48 import java.beans.PropertyChangeEvent;
49 import java.beans.PropertyChangeListener;
50 import java.io.File;
51 import java.lang.reflect.Method;
52 import java.net.URL;
53 import java.util.*;
54 import java.util.List;
55 import java.util.prefs.Preferences;
56 
57 // An amalgamation of editor and browser components to make a single frame
58 // for all purposes.  Looks like Aperture and LightRoom.
59 
60 public class ComboFrame
61     extends JFrame
62     implements ComponentListener,
63                DocumentDatabaseListener,
64                DocumentListener,
65                ImageBrowserListener,
66                ScaleListener,
67                WindowFocusListener,
68                TemplateControlListener,
69                MouseWheelListener {
70     // This is the frame icon, for frame decorations and the windows task bar:
71     public final static Image IconImage;
72     static {
73         URL url = ComboFrame.class.getResource("resources/LightZone.png");
74         IconImage = Toolkit.getDefaultToolkit().createImage(url);
75     }
76 
77     // Application often wants to know which was the most recently active:
78     static ComboFrame LastActiveComboFrame;
79 
80     // The data structure for the image under edit in this frame, or null if
81     // there is no active editor:
82     private Document doc;
83 
84     // The content pane of this frame, mainly polymorphic layout logic
85     private AbstractLayout layout;
86 
87     // The browser component:
88     private AbstractImageBrowser browser;
89 
90     // The data structure for the browser:
91     private ImageList images;
92 
93     // The tree widget with buttons that directs the browser:
94     private FolderCtrl folders;
95 
96     // The adapter that updates the browser on folder events:
97     private FolderTreeListener folderListener;
98 
99     // The scroll pane for the browser:
100     private ImageBrowserScrollPane browserScroll;
101 
102     // The metadata component:
103     private MetadataScroll info;
104 
105     // The editor structure, enabled or disabled:
106     private DisabledEditor disabledEditor;
107     private Editor editor;
108 
109     // The editor undo stack, enabled or disabled:
110     private DocUndoHistory history;
111 
112     // The template control, enabled or disabled:
113     private TemplateControl templates;
114 
115     // The toolbar structure, including some shared buttons:
116     private LayoutHeader header;
117 
118     // Some buttons (crop, rotate) require the "Tools" fading tab to show.
119     private PropertyChangeListener toolButtonListener;
120 
121     // The listeners and logic to trigger AdvisorDialogs.
122     private AdviceManager advice;
123 
124     // Depending on the perspective, we may or may not want to initialize
125     // the editor at browser selection events.
126     private boolean isEditorVisible;
127 
128     // Depending on the perspective, we may or may not want to run the browser
129     // background threads.
130     private boolean isBrowserVisible;
131 
132     // Menus that go with this frame:
133     private ComboFrameMenuBar menus;
134 
135     // Remember the current browser folder, for updating the title
136     private File recentFolder;
137 
138     // For debug mode only:
139     private MemoryMeter memory;
140 
141     // In case someone maximizes, quits, relaunches, and unmaximizes, we must
142     // know what unmaximized size to restore.
143     private Rectangle unMaximizedBounds;
144 
145     // In windowLostFocus() and windowGainedFocus(), the ImageList is paused
146     // and resumed.  This flag is set and reset in each case, so we can detect
147     // the initial, unbalanced gained focus.
148     private boolean focusPausedFlag;
149 
ComboFrame()150     ComboFrame() {
151         // Make sure LastActiveComboFrame is never null, for startup document
152         // open events that arrive before the first frame gains focus.
153         if (LastActiveComboFrame == null) {
154             LastActiveComboFrame = this;
155         }
156 
157         // Mac OS X 10.7 Lion Fullscreen Support
158         if (Platform.isMac()) {
159             enableFullScreenMode(this);
160         }
161 
162         // Java 1.6 will just use a cofee cup otherwise...
163         setIconImage(IconImage);
164 
165         // Update LastActiveAppFrame, pause background tasks:
166         addWindowFocusListener(this);
167 
168         // Keep track of the unmaximized bounds, so they can be saved in prefs:
169         addComponentListener(this);
170 
171         if (System.getProperty("lightcrafts.debug") != null) {
172             initMemoryMeter();
173         }
174         folders = new FolderCtrl();
175 
176         browserScroll = new ImageBrowserScrollPane();
177         // Don't let the split panes make the browser too small.
178         browserScroll.setMinimumSize(new Dimension(120, 120));
179 
180         disabledEditor = Document.createDisabledEditor(
181             new DisabledEditor.Listener() {
182                 public void imageClicked(Object key) {
183                     PreviewUpdater updater = (PreviewUpdater) key;
184                     File file = updater.getFile();
185                     Application.open(ComboFrame.this, file);
186                 }
187             }
188         );
189         editor = disabledEditor;
190 
191         // Don't let the split panes make the editor image too small.
192         editor.getImage().setMinimumSize(new Dimension(120, 120));
193 
194         history = new DocUndoHistory();
195 
196         templates = new TemplateControl(null, this);
197 
198         menus = new ComboFrameMenuBar(this);
199         setJMenuBar(menus);
200 
201         File folder = folders.getSelection();
202         if ((folder == null) || ! folder.exists()) {
203             if (folders.goToPicturesFolder()) {
204                 folder = folders.getSelection();
205             }
206         }
207         if ((folder == null) || ! folder.exists()) {
208             folder = new File(System.getProperty("user.home"));
209         }
210         advice = new AdviceManager(this);
211 
212         showFolder(folder, true);
213 
214         info = new MetadataScroll();
215         // The metadata table needs a preferred size for the initial layout:
216         info.setPreferredSize(new Dimension(250, 250));
217         info.setBackground(LightZoneSkin.Colors.ToolPanesBackground);
218 
219         setBackground(LightZoneSkin.Colors.FrameBackground);
220 
221         header = new LayoutHeader(this);
222 
223         layout = new BrowserLayout(
224             templates,
225             editor,
226             history,
227             folders,
228             browserScroll,
229             info,
230             header,
231             this
232         );
233         switch (layout.getLayoutType()) {
234             case Browser:
235                 isEditorVisible = false;
236                 isBrowserVisible = true;
237                 header.setBrowseSelected();
238                 break;
239             case Editor:
240                 isEditorVisible = true;
241                 isBrowserVisible = false;
242                 header.setEditSelected();
243                 break;
244             case Combo:
245                 isEditorVisible = true;
246                 isBrowserVisible = true;
247                 // Let modeButtons get initialized when layout changes.
248         }
249         if (! isBrowserVisible) {
250             images.pause();
251         }
252         folderListener = new FolderTreeListener() {
253             File recentFolder;
254             public void folderSelectionChanged(File folder) {
255                 if ((folder != null) && folder.equals(recentFolder)) {
256                     // The folder monitor mechanism causes spurious
257                     // folder selection events.
258                     return;
259                 }
260                 recentFolder = folder;
261                 // Persist the new path:
262                 saveFolder(folder);
263                 // Add a reference to this path to the recent menus:
264                 Application.notifyRecentFolder(folder);
265                 // Update this frame:
266                 showFolder(folder, true);
267             }
268             public void folderDropAccepted(
269                 final List<File> files, final File folder
270             ) {
271                 // We must handle the drop on a much later event queue task,
272                 // because we get our flag saying whether to move or copy from
273                 // the browser, which itself only finds out in a DragSource
274                 // callback that happens after the drop is completed.
275                 EventQueue.invokeLater(
276                     new Runnable() {
277                         public void run() {
278                             EventQueue.invokeLater(
279                                 new Runnable() {
280                                     public void run() {
281                                         int copyOrMove = browser.wasCopyOrMove();
282                                         switch (copyOrMove) {
283                                             case DnDConstants.ACTION_MOVE:
284                                                 Application.moveFiles(
285                                                     ComboFrame.this, files, folder
286                                                 );
287                                                 break;
288                                             case DnDConstants.ACTION_COPY:
289                                                 Application.copyFiles(
290                                                     ComboFrame.this, files, folder
291                                                 );
292                                                 break;
293                                         }
294                                     }
295                                 }
296                             );
297                         }
298                     }
299                 );
300             }
301         };
302         folders.addSelectionListener(folderListener);
303 
304         setDocument(null);
305 
306         DocumentDatabase.addListener(this);
307 
308         // Disabled editor text depends on the browser initialization and
309         // the layout.
310         String disabledText = getDisabledEditorText();
311         editor.setDisabledText(disabledText);
312 
313         updateTitle();
314     }
315 
addTemplate()316     public void addTemplate() {
317         String namespace = templates.getNamespace();
318         TemplateKey key = Application.saveTemplate(ComboFrame.this, namespace);
319         if (key != null) {
320             namespace = key.getNamespace();
321             templates.setNamespace(namespace);
322         }
323     }
324 
refresh()325     public void refresh() {
326         File folder = folders.getSelection();
327         if (folder != null) {
328             showFolder(folder, false);
329         }
330     }
331 
getDocument()332     public Document getDocument() {
333         return doc;
334     }
335 
getEditor()336     public Editor getEditor() {
337         return editor;
338     }
339 
getBrowser()340     public AbstractImageBrowser getBrowser() {
341         return browser;
342     }
343 
getIconImage()344     public Image getIconImage() {
345         return IconImage;
346     }
347 
348     // Called from the constructor, the folder selection listener, and
349     // refresh().  Disposes the current browser, creates a new one, initializes
350     // its selection, and replaces the old browser with the new one in the
351     // layout.
showFolder(File folder, boolean useCache)352     void showFolder(File folder, boolean useCache) {
353         unsetBrowser();
354         if (images != null) {
355             images.stop();
356         }
357         if (doc == null) {
358             ((DisabledEditor) editor).removeImages();
359         }
360         if (info != null) {
361             info.endEditing();
362         }
363 
364         editor.hideWait();
365 
366         ImageList oldImages = images;
367 
368         initImages(folder, useCache);
369 
370         browser = BrowserFactory.createRecent(images);
371         setBrowser();   // Puts browser as the viewport in browserScroll
372 
373         initBrowserSelection(folder);
374 
375         images.start();
376 
377         if (oldImages != null) {
378             // migrate the pause depth to the new browser
379             int pauses = oldImages.getPauseDepth();
380             for (int n=0; n<pauses; n++) {
381                 images.pause();
382             }
383         }
384         menus.update();
385 
386         String disabledText = getDisabledEditorText();
387         editor.setDisabledText(disabledText);
388 
389         recentFolder = folder;
390 
391         updateTitle();
392     }
393 
setBrowserCollapsed()394     void setBrowserCollapsed() {
395         if (! BrowserFactory.isCollapsed(browser)) {
396             ArrayList<File> files = browser.getSelectedFiles();
397             unsetBrowser();
398             browser = BrowserFactory.createCollapsed(images);
399             setBrowser();
400             browser.setSelectedFiles(files);
401             menus.update();
402         }
403     }
404 
setBrowserExpanded()405     void setBrowserExpanded() {
406         if (BrowserFactory.isCollapsed(browser)) {
407             ArrayList<File> files = browser.getSelectedFiles();
408             unsetBrowser();
409             browser = BrowserFactory.createExpanded(images);
410             setBrowser();
411             browser.setSelectedFiles(files);
412             menus.update();
413         }
414     }
415 
setImage(ImageInfo imageInfo)416     public void setImage(ImageInfo imageInfo) {
417         info.setImage(imageInfo);
418     }
419 
420     // Implementing ImageBrowserListener.  Updates the metadata display,
421     // shuts down any open Document, shows a preview if the shutdown was
422     // successful, remembers the selection in preferences, and updates the
423     // frame title.
selectionChanged(ImageBrowserEvent event)424     public void selectionChanged(ImageBrowserEvent event) {
425         File file = event.getFile();
426         if (file != null) {
427             ImageInfo imageInfo = ImageInfo.getInstanceFor(file);
428             setImage(imageInfo);
429             BrowserSelectionMemory.setRememberedFile(file);
430         }
431         else {
432             setImage(null);
433         }
434         // Attempt to put away any current editor:
435         if (doc != null) {
436             SaveResult saved = SaveResult.Saved;
437             if (doc.isDirty()) {
438                 saved = autoSave();
439             }
440             if (
441                 saved == SaveResult.Saved ||
442                 saved == SaveResult.DontSave
443             ) {
444                 Application.closeDocument(this);
445             }
446         }
447         // If the editor is disabled, then show previews:
448         if (doc == null) {
449             // Remove any old previews:
450             ((DisabledEditor) editor).removeImages();
451             // Take away the "Click to Edit" message:
452             editor.hideWait();
453 
454             // Get the current previews:
455             List<PreviewUpdater> previews = event.getSelectedPreviews();
456 
457             // See if the user has selected a large number of new images:
458             int previewCount = previews.size();
459             if (previewCount > 8) {
460                 // We don't show previews at bulk selection events, assuming
461                 // the user made the selection for some purpose other than
462                 // seeing previews.
463                 String text = LOCALE.get("TooManyPreviewsText", previewCount);
464                 editor.setDisabledText(text);
465                 return;
466             }
467             else {
468                 String text = getDisabledEditorText();
469                 editor.setDisabledText(text);
470             }
471             for (PreviewUpdater preview : previews) {
472                 // Install each preview in the DisabledEditor display:
473                 RenderedImage image = preview.getImage(
474                     new PreviewUpdater.Observer() {
475                         public void imageChanged(
476                             PreviewUpdater updater, RenderedImage image
477                         ) {
478                             // This callback could come at any time,
479                             // maybe even after a Document has opened.
480                             if (editor instanceof DisabledEditor) {
481                                 ((DisabledEditor) editor).updateImage(
482                                     updater, image
483                                 );
484                             }
485                         }
486                     }
487                 );
488                 ((DisabledEditor) editor).addImage(preview, image);
489             }
490         }
491         menus.update();
492     }
493 
494     // Implementing ImageBrowserListener.  Shuts down any open Document,
495     // and if the shutdown was successful, shows a preview of the new file,
496     // and then initiates the open-Document sequence.
imageDoubleClicked(ImageBrowserEvent event)497     public void imageDoubleClicked(ImageBrowserEvent event) {
498         File file = event.getFile();
499         if (file == null) {
500             return;
501         }
502         ComboFrame frame = Application.getFrameForFile(file);
503         if (frame != null) {
504             frame.requestFocus();
505             return;
506         }
507         // Attempt to put away any current editor:
508         if (doc != null) {
509             SaveResult saved = SaveResult.Saved;
510             if (doc.isDirty()) {
511                 saved = autoSave();
512             }
513             if (
514                 saved == SaveResult.Saved ||
515                 saved == SaveResult.DontSave
516             ) {
517                 Application.closeDocument(this);
518             }
519         }
520         // If the close was successful, then update:
521         if (doc == null) {
522             Application.open(file, this, null);
523         }
524     }
525 
526     // Part of the convoluted event handling for horizontal-scroll events.
527     // See mouseWheelMoved().
isMouseWheelEventInComponent( MouseWheelEvent e, JComponent comp )528     private boolean isMouseWheelEventInComponent(
529         MouseWheelEvent e, JComponent comp
530     ) {
531         if (comp.isShowing()) {
532             Point compLoc = comp.getLocationOnScreen();
533             Point frameLoc = getLocationOnScreen();
534             Rectangle bounds = new Rectangle(
535                 compLoc.x - frameLoc.x,
536                 compLoc.y - frameLoc.y,
537                 comp.getWidth(),
538                 comp.getHeight()
539             );
540             return bounds.contains(e.getPoint());
541         }
542         return false;
543     }
544 
545     // Propagate the special horizontal-scroll mouse wheel events (Mighty
546     // Mouse, two-finger trackpad gesture) to scrollable descendants.
547     // This should only be called from the Platform class on Mac.
mouseWheelMoved(MouseWheelEvent e)548     public void mouseWheelMoved(MouseWheelEvent e) {
549         if (e.getScrollType() == 2) {
550             if (editor != null) {
551                 JComponent image = editor.getImage();
552                 if (isMouseWheelEventInComponent(e, image)) {
553                     editor.horizontalMouseWheelMoved(e);
554                     return;
555                 }
556             }
557             if (folders != null) {
558                 JComponent tree = folders.getTree();
559                 if (isMouseWheelEventInComponent(e, tree)) {
560                     folders.horizontalMouseWheelMoved(e);
561                     return;
562                 }
563             }
564             if (history != null) {
565                 JComponent scroll = history.getScrollPane();
566                 if (isMouseWheelEventInComponent(e, scroll)) {
567                     history.horizontalMouseWheelMoved(e);
568                     return;
569                 }
570             }
571             if (templates != null) {
572                 JComponent scroll = templates.getScrollPane();
573                 if (isMouseWheelEventInComponent(e, scroll)) {
574                     templates.horizontalMouseWheelMoved(e);
575                 }
576             }
577         }
578     }
579 
browserError(String message)580     public void browserError(String message) {
581     }
582 
getRecentFolder()583     public File getRecentFolder() {
584         return recentFolder;
585     }
586 
showWait(String text)587     void showWait(String text) {
588         editor.showWait(text);
589     }
590 
hideWait()591     void hideWait() {
592         editor.hideWait();
593     }
594 
595     // Updates the editor to edit the given Document, or disables the editor
596     // if the argument is null.  Called from Application show() and close()
597     // methods, and also from the ComboFrame constructor.
setDocument(Document doc)598     void setDocument(Document doc) {
599         if (this.doc != null) {
600             ScaleModel scale = this.doc.getScaleModel();
601             scale.removeScaleListener(this);
602             this.doc.removeDocumentListener(this);
603             this.doc.getProofAction().
604                 removePropertyChangeListener(toolButtonListener);
605             toolButtonListener = null;
606         }
607         else {
608             if (editor != null) {
609                 ((DisabledEditor) editor).dispose();
610             }
611         }
612         if (advice != null) {
613             advice.dispose();
614         }
615         this.doc = doc;
616 
617         advice = new AdviceManager(this);
618 
619         if (doc != null) {
620             editor = doc.getEditor();
621             history = new DocUndoHistory(doc);
622             templates.dispose();
623             templates = new TemplateControl(editor, this);
624             ScaleModel scale = doc.getScaleModel();
625             scale.addScaleListener(this);
626             doc.addDocumentListener(this);
627             OtherApplication source = (OtherApplication) doc.getSource();
628             if ((! isEditorVisible) || (source != null)) {
629                 showEditorPerspective();
630             }
631             toolButtonListener =
632                 new PropertyChangeListener() {
633                     public void propertyChange(PropertyChangeEvent event) {
634                         String propName = event.getPropertyName();
635                         if (propName.equals(ToggleAction.TOGGLE_STATE)) {
636                             boolean selected = (Boolean) event.getNewValue();
637                             if (selected && (layout instanceof EditorLayout)) {
638                                ((EditorLayout) layout).ensureToolsVisible();
639                             }
640                         }
641                     }
642                 };
643             // Don't let the split panes make the editor image too small.
644             editor.getImage().setMinimumSize(new Dimension(120, 120));
645 
646             doc.getProofAction().addPropertyChangeListener(toolButtonListener);
647         }
648         else {
649             editor = disabledEditor;
650 
651             // Don't let the split panes make the editor image too small.
652             editor.getImage().setMinimumSize(new Dimension(120, 120));
653 
654             String disabledText = getDisabledEditorText();
655             editor.setDisabledText(disabledText);
656 
657             history = new DocUndoHistory();
658 
659             templates.dispose();
660             templates = new TemplateControl(null, this);
661 
662             // TODO: we come here from showBrowserPerspective...
663             if (! isBrowserVisible) {
664                 showBrowserPerspective();
665             }
666         }
667         if (memory != null) {
668             JComponent toolbar = editor.getToolBar();
669             toolbar.add(memory);
670         }
671         layout.updateEditor(templates, editor, history, info);
672 
673         setContentPane(layout);
674         header.update();
675         menus.update();
676         updateTitle();
677 
678         if (layout.getLayoutType() == AbstractLayout.LayoutType.Combo) {
679             browser.requestFocusInWindow();
680         }
681     }
682 
openSelected()683     public boolean openSelected() {
684         // If there is a preview showing, then open the preview for editing.
685         if (editor instanceof DisabledEditor) {
686             PreviewUpdater updater =
687                 (PreviewUpdater) ((DisabledEditor) editor).getLastKey();
688             if (updater != null) {
689                 File file = updater.getFile();
690                 Application.open(this, file);
691                 return true;
692             }
693         }
694         return false;
695     }
696 
showEditorPerspective()697     public void showEditorPerspective() {
698         layout.dispose();
699         layout = new EditorLayout(
700             templates,
701             editor,
702             history,
703             folders,
704             browserScroll,
705             info,
706             header
707         );
708         layout.updateEditor(templates, editor, history, info);
709 
710         repaint();
711         setContentPane(layout);
712         validate();
713 
714         requestFocusInWindow();
715 
716         if (isBrowserVisible) {
717             pause();
718             isBrowserVisible = false;
719         }
720         isEditorVisible = true;
721 
722         header.setEditSelected();
723 
724         updateDisabledEditorText();
725 
726         menus.update();
727     }
728 
closeDocument()729     public boolean closeDocument() {
730         if (doc != null) {
731 //            // If an unapplied template is selected, reset it before committing
732 //            // any changes.
733 //            templates.clearSelection();
734 
735             SaveResult saved = autoSave();
736 
737             switch (saved) {
738                 case Saved:
739                 case DontSave:
740                     // Could just call setDocument(null), but
741                     // Application.closeDocument() disposes the Document.
742                     Application.closeDocument(this);
743                     break;
744                 case CouldntSave:
745                     switch (Application.saveAs(this)) {
746                         case Saved:
747                         case DontSave:
748                             return true;
749                         case CouldntSave:
750                         case Cancelled:
751                             return false;
752                     }
753                 case Cancelled:
754                     return false;
755             }
756         }
757         return true;
758     }
759 
760     // A false return value means that the perspective change was cancelled by
761     // the user.
showBrowserPerspective()762     public boolean showBrowserPerspective() {
763         if (! closeDocument()) {
764             return false;
765         }
766         if (! isBrowserVisible) {
767             layout.dispose();
768             layout = new BrowserLayout(
769                 templates,
770                 editor,
771                 history,
772                 folders,
773                 browserScroll,
774                 info,
775                 header,
776                 this
777             );
778 
779             browser.justShown = true;
780 
781             repaint();
782             setContentPane(layout);
783             validate();
784 
785             browser.requestFocusInWindow();
786 
787             if (! isBrowserVisible) {
788                 resume();
789                 isBrowserVisible = true;
790             }
791             isEditorVisible = false;
792 
793             File file = browser.getLeadSelectedFile();
794             ImageInfo imageInfo = ImageInfo.getInstanceFor(file);
795             setImage(imageInfo);
796 
797             header.setBrowseSelected();
798 
799             updateDisabledEditorText();
800 
801             menus.update();
802         }
803         return true;
804     }
805 
showComboPerspective()806     public void showComboPerspective() {
807         layout.dispose();
808         layout = new ComboLayout(
809             templates,
810             editor,
811             history,
812             folders,
813             browserScroll,
814             info,
815             header,
816             this
817         );
818         setContentPane(layout);
819         validate();
820         repaint();
821 
822         browser.requestFocusInWindow();
823 
824         if (! isBrowserVisible) {
825             resume();
826             isBrowserVisible = true;
827         }
828         isEditorVisible = true;
829 
830         updateDisabledEditorText();
831 
832         menus.update();
833     }
834 
835     // The browser menu may want to suppress things in editor layout
isBrowserVisible()836     public boolean isBrowserVisible() {
837         return isBrowserVisible;
838     }
839 
840     // *** DocumentListener start ***
841 
documentChanged(Document doc, boolean isDirty)842     public void documentChanged(Document doc, boolean isDirty) {
843         updateTitle();
844     }
845 
846     // *** DocumentListener end ***
847 
848     // *** ScaleModelListener start ***
849 
scaleChanged(Scale scale)850     public void scaleChanged(Scale scale) {
851         updateTitle();
852     }
853 
854     // *** ScaleModelListener end ***
855 
856     // *** DocumentDatabaseListener start ***
857 
docFilesChanged(File imageFile)858     public void docFilesChanged(File imageFile) {
859         if (browser != null) {
860             // Regroup images in the browser, but only if the changed file is
861             // in the directory being browsed.
862             File folder = imageFile.getParentFile();
863             if ((folder != null) && folder.equals(recentFolder)) {
864                 images.regroup();
865             }
866         }
867     }
868 
869     // *** DocumentDatabaseListener end ***
870 
871     // Called from showFolder().
updateTitleNoDoc()872     private void updateTitleNoDoc() {
873         if (doc != null) {
874             return;
875         }
876         StringBuffer buffer = new StringBuffer();
877         if (recentFolder != null) {
878             String name =
879                 Platform.getPlatform().getDisplayNameOf( recentFolder );
880             buffer.append(name);
881             buffer.append(" - ");
882         }
883         buffer.append(Version.getApplicationName());
884 
885         setTitle(buffer.toString());
886 
887         WindowMenu.updateAll();
888     }
889 
updateTitleDoc()890     public void updateTitleDoc() {
891         StringBuffer sb = new StringBuffer();
892         boolean dirty = (doc != null) && doc.isDirty();
893         if (dirty) {
894             sb.append("* ");
895         }
896         if (doc != null) {
897             File file = doc.getFile();
898             ImageMetadata meta = doc.getMetadata();
899             if (file == null) {
900                 file = meta.getFile();
901             }
902             String name = file.getName();
903             String imageName = meta.getFile().getName();
904             if (! name.equals(imageName)) {
905                 name = name + " [" + imageName + "]";
906             }
907             sb.append(name);
908             ScaleModel scaleModel = doc.getScaleModel();
909             Scale scale = scaleModel.getCurrentScale();
910             sb.append(" (").append(scale).append(")");
911             sb.append(" - ");
912         }
913         sb.append(Version.getApplicationName());
914 
915         setTitle(sb.toString());
916 
917         WindowMenu.updateAll();
918 
919         // Handle the little-known close button dot for dirty windows on Mac:
920         JRootPane root = getRootPane();
921         root.putClientProperty(
922             "windowModified", dirty ? Boolean.TRUE : Boolean.FALSE
923         );
924     }
925 
926     // Called from setDocument(), scaleChanged(), documentChanged(),
927     // Application.showPreferences(), the relicensing under the Help menu,
928     // and the constructor.
updateTitle()929     public void updateTitle() {
930         if (doc != null) {
931             updateTitleDoc();
932         }
933         else {
934             updateTitleNoDoc();
935         }
936     }
937 
938     // Called from Application before Document initialization, when entering
939     // the editor perspective, during file I/O that may wake the browser,
940     // and at the start of batch processing.
pause()941     public void pause() {
942         if (images != null) {
943             images.pause();
944         }
945     }
946 
947     // Called from Application after Document initialization, when exiting
948     // the editor perspective, after file I/O that may wake the browser, and
949     // after batch processing.
resume()950     public void resume() {
951         if (images != null) {
952             images.resume();
953         }
954         // Encourage JVM to release free heap space
955         System.gc();
956     }
957 
dispose()958     public void dispose() {
959         super.dispose();
960         if (memory != null) {
961             memory.dispose();
962         }
963         images.stop();
964 
965         if (browser != null) {
966             unsetBrowser();
967         }
968 
969         remove(menus);
970         setJMenuBar(null);
971         menus.dispose();
972 
973         templates.dispose();
974 
975         folders.removeSelectionListener(folderListener);
976         folders.dispose();
977 
978         if (advice != null) {
979             advice.dispose();
980             advice = null;
981         }
982         // Document.dispose() calls DocPanel.dispose(), but if we instantiated
983         // the DocPanel ourselves, then we must clean it up:
984         if (doc == null) {
985             ((DisabledEditor) editor).dispose();
986         }
987         else {
988             // Sever connection between Document actions and this frame.
989             doc.getProofAction().
990                 removePropertyChangeListener(toolButtonListener);
991         }
992         header.dispose();
993         layout.dispose();
994 
995         // Sever references, to fix leaks associated with lingering
996         // DocFrame instances:
997         setContentPane(new JPanel());
998         doc = null;
999         images = null;
1000         layout = null;
1001         browser = null;
1002         editor = null;
1003         folders = null;
1004         browserScroll = null;
1005         info = null;
1006         header = null;
1007 
1008         if (LastActiveComboFrame == this) {
1009             LastActiveComboFrame = null;
1010         }
1011     }
1012 
1013     // Sometimes the focus listener gets called after dispose().
isDisposed()1014     private boolean isDisposed() {
1015         return browser == null;
1016     }
1017 
1018     // Initialize the ImageList data for the browser from the given directory
1019     // under a ProgressDialog.
initImages(final File directory, final boolean useCache)1020     private void initImages(final File directory, final boolean useCache) {
1021         ProgressDialog dialog = Platform.getPlatform().getProgressDialog();
1022         ProgressThread thread = new ProgressThread(dialog) {
1023             public void run() {
1024                 DocumentDatabase.addDocumentDirectory(directory);
1025                 images = new ImageList(
1026                     directory,
1027                     100,
1028                     FileCacheFactory.get(directory),
1029                     useCache,
1030                     ImageDatumComparator.CaptureTime,
1031                     getProgressIndicator()
1032                 );
1033             }
1034             public void cancel() {
1035                 ImageList.cancel();
1036             }
1037         };
1038         FileSystemView view = Platform.getPlatform().getFileSystemView();
1039         String dirName = view.getSystemDisplayName(directory);
1040         dialog.showProgress(
1041             this, thread, LOCALE.get("ScanningMessage", dirName), 0, 1, true
1042         );
1043         Throwable t = dialog.getThrown();
1044         if (t != null) {
1045             throw new RuntimeException(LOCALE.get("ScanningError"), t);
1046         }
1047         if (images.getAllImageData().isEmpty()) {
1048             // Enqueue this, because the layout may be getting initialized
1049             // on this same task.
1050             EventQueue.invokeLater(
1051                 new Runnable() {
1052                     public void run() {
1053                         if (layout instanceof BrowserLayout) {
1054                             ((BrowserLayout) layout).ensureFoldersVisible();
1055                             advice.showEmptyFolderAdvice();
1056                         }
1057                     }
1058                 }
1059             );
1060         }
1061         else {
1062             advice.hideEmptyFolderAdvice();
1063         }
1064     }
1065 
1066     // Do the things we do when there is a new browser.
setBrowser()1067     private void setBrowser() {
1068         browser.addBrowserListener(this);
1069 
1070         browser.setTemplateProvider(templateProvider);
1071         browser.addBrowserAction(exportAction);
1072         browser.addBrowserAction(printAction);
1073         browser.setImageGroupProvider(new LznImageGroupProvider());
1074         browser.setPreviewProvider(new LznPreviewProvider(this));
1075 
1076         browserScroll.setBrowser(browser);
1077 
1078         if (layout != null) {
1079             // This method may be called at browser initialization, before the
1080             // layout member is initialized.
1081             header.update();
1082             layout.updateBrowser();
1083         }
1084         // Now here is a mystery: a repaint() at this time does not result in a
1085         // call to paint() on the browser.  The call works on a subsequent task.
1086         //
1087         // Also initialize the browser selection on a subsequent task, rather
1088         // than propagating a selection change from here.
1089         EventQueue.invokeLater(
1090             new Runnable() {
1091                 public void run() {
1092                     browser.repaint();
1093                 }
1094             }
1095         );
1096     }
1097 
1098     // Undo the things we do when there is a new browser.
unsetBrowser()1099     private void unsetBrowser() {
1100         if (browser != null) {
1101             browser.removeBrowserListener(this);
1102             BrowserFactory.dispose(browser);
1103         }
1104     }
1105 
updateDisabledEditorText()1106     private void updateDisabledEditorText() {
1107         if (editor instanceof DisabledEditor) {
1108             String text = getDisabledEditorText();
1109             editor.setDisabledText(text);
1110         }
1111     }
1112 
initBrowserSelection(File folder)1113     private void initBrowserSelection(File folder) {
1114         // Set an initial selection in the browser, from prefs if possible
1115         File file = BrowserSelectionMemory.getRememberedFile(folder);
1116         if (file == null) {
1117             Collection<ImageDatum> datums = images.getAllImageData();
1118             if (! datums.isEmpty()) {
1119                 ImageDatum datum = images.getAllImageData().get(0);
1120                 file = datum.getFile();
1121             }
1122         }
1123         if (file != null) {
1124             // Enqueue, because this method is called during the constructor.
1125             final Collection<File> files = Collections.singleton(file);
1126             EventQueue.invokeLater(
1127                 new Runnable() {
1128                     public void run() {
1129                         // This task may run after dispose().
1130                         if (browser != null) {
1131                             browser.setSelectedFiles(files);
1132                         }
1133                     }
1134                 }
1135             );
1136         }
1137     }
1138 
getDisabledEditorText()1139     private String getDisabledEditorText() {
1140         if (images == null) {
1141             // This gets called during shutdown.
1142             return "";
1143         }
1144         switch (AbstractLayout.getRecentLayoutType()) {
1145             case Editor:
1146                 // "Open an Image"
1147                 return LOCALE.get("EditorLayoutDisabledEditorText");
1148             case Combo:
1149                 if (images.getAllImageData().size() > 0) {
1150                     // "Select an Image"
1151                     return LOCALE.get("ComboLayoutDisabledEditorText");
1152                 }
1153                 // (no message; the browser will have one instead)
1154                 return null;
1155             default:
1156                 if (images.getAllImageData().size() > 0) {
1157                     // "Select an Image"
1158                     return LOCALE.get("BrowserLayoutDisabledEditorText");
1159                 }
1160                 // (no message; the browser will have one instead)
1161                 return null;
1162         }
1163     }
1164 
autoSave()1165     private SaveResult autoSave() {
1166         SaveResult result = SaveResult.Saved;
1167         if (doc != null) {
1168             if (doc.isDirty()) {
1169                 SaveOptions options = doc.getSaveOptions();
1170                 if (options == null) {
1171                     if (OtherApplicationShim.shouldSaveDirectly(doc)) {
1172                         options = OtherApplicationShim.createExportOptions(doc);
1173                     }
1174                     else {
1175                         options = Application.getSaveOptions(doc);
1176                     }
1177                 }
1178                 Preferences prefs = Preferences.userRoot().node(
1179                     "/com/lightcrafts/app"
1180                 );
1181                 boolean autoSave = prefs.getBoolean("AutoSave", true);
1182                 if (autoSave) {
1183                     doc.setSaveOptions(options);
1184                     result = Application.save(this) ?
1185                         SaveResult.Saved : SaveResult.CouldntSave;
1186                 }
1187                 else {
1188                     JLabel prompt = new JLabel(
1189                         LOCALE.get(
1190                             "AutoSaveQuestion", options.getFile().getName()
1191                         )
1192                     );
1193 
1194                     Box message = Box.createVerticalBox();
1195                     message.add(prompt);
1196                     int dialogOption = UICompliance.showOptionDialog(
1197                         this,
1198                         message,
1199                         LOCALE.get("AutoSaveDialogTitle"),
1200                         JOptionPane.DEFAULT_OPTION,
1201                         JOptionPane.QUESTION_MESSAGE,
1202                         null,
1203                         new Object[] {
1204                             LOCALE.get("AutoSaveSaveOption"),
1205                             LOCALE.get("AutoSaveSaveAsOption"),
1206                             LOCALE.get("AutoSaveCancelOption"),
1207                             LOCALE.get("AutoSaveDontSaveOption")
1208                         },
1209                         LOCALE.get("AutoSaveSaveOption"), 3
1210                     );
1211 
1212                     switch (dialogOption) {
1213                         case 0:
1214                             // "save"
1215                             doc.setSaveOptions(options);
1216                             result = Application.save(this) ?
1217                                 SaveResult.Saved : SaveResult.CouldntSave;
1218                             break;
1219                         case 1:
1220                             // "save elsewhere"
1221                             result = Application.saveAs(this);
1222                             break;
1223                         case 2:
1224                         case -1:
1225                             // "cancel"
1226                             result = SaveResult.Cancelled;
1227                             break;
1228                         case 3:
1229                             // "don't save"
1230                             Application.closeDocumentForce(this);
1231                             result = SaveResult.DontSave;
1232                             break;
1233                     }
1234                 }
1235             }
1236         }
1237         return result;
1238     }
1239 
1240     // *** WindowFocusListener start ***
1241     //
1242     //     Pause and resume browser background tasks and polling, and keep
1243     //     the TemplateControl up to date.
1244 
windowGainedFocus(WindowEvent event)1245     public void windowGainedFocus(WindowEvent event) {
1246         if (! isDisposed()) {
1247             LastActiveComboFrame = this;
1248             if (focusPausedFlag) {
1249                 resume();
1250                 focusPausedFlag = false;
1251             }
1252             folders.resumeFolderMonitor();
1253             templates.refresh();
1254         }
1255     }
1256 
windowLostFocus(WindowEvent event)1257     public void windowLostFocus(WindowEvent event) {
1258         if (! isDisposed()) {
1259             if (! focusPausedFlag) {
1260                 pause();
1261                 focusPausedFlag = true;
1262             }
1263             folders.pauseFolderMonitor();
1264         }
1265     }
1266 
1267     // *** WindowFocusListener end **
1268 
1269     // *** ComponentListener start ***
1270     //
1271     //     Keep track of our unmaximizd bounds so they can be persisted.
1272 
componentResized(ComponentEvent e)1273     public void componentResized(ComponentEvent e) {
1274         int state = getExtendedState();
1275         if (state == JFrame.NORMAL) {
1276             unMaximizedBounds = getBounds();
1277         }
1278     }
1279 
componentShown(ComponentEvent e)1280     public void componentShown(ComponentEvent e) {
1281     }
1282 
componentHidden(ComponentEvent e)1283     public void componentHidden(ComponentEvent e) {
1284     }
1285 
componentMoved(ComponentEvent e)1286     public void componentMoved(ComponentEvent e) {
1287         int state = getExtendedState();
1288         if (state == JFrame.NORMAL) {
1289             unMaximizedBounds = getBounds();
1290         }
1291     }
1292 
getUnmaximizedBounds()1293     Rectangle getUnmaximizedBounds() {
1294         // The unMaximizedBounds is only initialized after the frame validates.
1295         if (unMaximizedBounds != null) {
1296             return new Rectangle(unMaximizedBounds);
1297         }
1298         return null;
1299     }
1300 
1301     // *** ComponentListener end ***
1302 
1303     // *** Folder tree path persistence start ***
1304     //
1305     //     This supports the "Recent Folders" mechanism in the File menu.
1306 
1307     // Trigger a folder navigation, starting with the folder tree.
1308     // Called via the Recent Folders item and Application.openFolder().
showFolder(File folder)1309     void showFolder(File folder) {
1310         if (!folders.goToFolder(folder)) {
1311             return;
1312         }
1313         String key = getKeyForFolder(folder);
1314         folders.restorePath(key);
1315         // This triggers the selection listener, which updates things.
1316         if (! isBrowserVisible) {
1317             // Make sure the browser is showing:
1318             showBrowserPerspective();
1319         }
1320     }
1321 
1322     // Save the currently selected folder tree path, for later access in
1323     // the Recent Folders item.
saveFolder(File folder)1324     void saveFolder(File folder) {
1325         // Don't rely on folders.getSelection(), which isn't current during the
1326         // selection listener callback.
1327         if (folder != null && folders.goToFolder(folder)) {
1328             String key = getKeyForFolder(folder);
1329             folders.savePath(key);
1330         }
1331     }
1332 
1333     // Clear a persistent path previously saved in saveFolder().  Called from
1334     // Application.addToRecentFolders().
clearFolder(File folder)1335     static void clearFolder(File folder) {
1336         String key = getKeyForFolder(folder);
1337         FolderBrowserPane.clearPath(key);
1338     }
1339 
getKeyForFolder(File folder)1340     private static String getKeyForFolder(File folder) {
1341         String path = folder.getAbsolutePath();
1342         int hash = path.hashCode();
1343         return Integer.toString(hash);
1344     }
1345 
1346     // *** Folder tree path persistence end ***
1347 
initMemoryMeter()1348     private void initMemoryMeter() {
1349         if (memory == null) {
1350             memory = new MemoryMeter();
1351             Border empty = BorderFactory.createEmptyBorder(6, 0, 6, 0);
1352             Border line = BorderFactory.createLineBorder(Color.gray);
1353             Border compound = BorderFactory.createCompoundBorder(empty, line);
1354             memory.setBorder(compound);
1355         }
1356     }
1357 
1358     // *** Helper interface implementations for use in the browser: start. ***
1359 
1360     private TemplateProvider templateProvider = new TemplateProvider() {
1361         public List getTemplateActions() {
1362             try {
1363                 return TemplateDatabase.getTemplateKeys();
1364             }
1365             catch (TemplateDatabase.TemplateException e) {
1366                 // Templates are broken, abort.
1367                 e.printStackTrace();
1368                 return new ArrayList();
1369             }
1370         }
1371         public void applyTemplateAction(Object action, File[] targets) {
1372             TemplateKey key = (TemplateKey) action;
1373             Application.applyTemplate(ComboFrame.this, targets, key);
1374         }
1375         public void applyTemplate(File file, File[] targets) {
1376             Application.applyTemplate(ComboFrame.this, targets, file);
1377         }
1378     };
1379 
1380     private ExternalBrowserAction printAction = new ExternalBrowserAction() {
1381         public String getName() {
1382             return LOCALE.get("BrowserPrintMenuItem");
1383         }
1384         public void actionPerformed(File file, File[] files) {
1385             Application.print(ComboFrame.this, file);
1386         }
1387     };
1388 
1389     private ExternalBrowserAction exportAction = new ExternalBrowserAction() {
1390         public String getName() {
1391             return LOCALE.get("BrowserExportMenuItem");
1392         }
1393         public void actionPerformed(File file, File[] files) {
1394             if (files.length == 1) {
1395                 Application.export(ComboFrame.this, file);
1396             }
1397             else {
1398                 Application.export(ComboFrame.this, files);
1399             }
1400         }
1401     };
1402 
1403     // *** Helper interface implementations for use in the browser: end. ***
1404 
enableFullScreenMode(Window window)1405     public static void enableFullScreenMode(Window window) {
1406         try {
1407             Class<?> clazz = Class.forName("com.apple.eawt.FullScreenUtilities");
1408             Class<?> param[] = new Class<?>[] { Window.class, Boolean.TYPE };
1409             Method method = clazz.getMethod("setWindowCanFullScreen", param);
1410             method.invoke(clazz, window, true);
1411         }
1412         catch (ClassNotFoundException e0) {
1413 	    // Just ignore it, may be the OS is older than 10.7 Lion
1414         }
1415         catch (Exception e) {
1416             System.err.println("Could not enable OS X fullscreen mode " + e);
1417         }
1418     }
1419 }
1420