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