/* * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. * Copyright (C) 2011 Peransin Nicolas. All rights reserved. * Use is subject to license terms. */ package examples; import java.awt.Dimension; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.EventObject; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileNameExtensionFilter; import org.mypsycho.swing.app.Action; import org.mypsycho.swing.app.Application; import org.mypsycho.swing.app.ApplicationListener; import org.mypsycho.swing.app.FrameView; import org.mypsycho.swing.app.SingleFrameApplication; import org.mypsycho.swing.app.beans.MenuFrame; import org.mypsycho.swing.app.task.Task; import org.mypsycho.text.TextMap; /** * This is a very simple example of a SingleFrameApplication that * loads and saves a single text document. Although it does not * possess all of the usual trappings of a single-document app, * like versioning or support for undo/redo, it does serve * to highlight how to use actions, resources, and tasks. * * <p> * The application's state is defined by two read-only bound properties: * <dl> * <dt><strong>File {@link #getFile file}</strong></dt> * <dd>The current text File being edited.</dd> * <dt><strong>boolean {@link #isModified modified}</strong></dt> * <dd>True if the current file needs to be saved.</dd> * </dl> * These properties are updated when the user interacts with the * application. They can be used as binding sources, to monitor * the application's state. * </p> * <p> * The application is {@link Application#launch launched} in the * main method on the "main" thread. All the work of actually * constructing, {@link #initialize intializing}, and * {@link #startup starting} the application actually * happens on the EDT. * </p> * <p> * The resources for this Application are defined in {@code * resources/DocumentExample.properties}. * </p> * <p> * This application defines a small set of actions for opening * and saving files: {@link #open open}, {@link #save save}, * and {@link #saveAs saveAs}. It inherits * {@code cut/copy/paste/delete} ProxyActions from the * {@code Application} class. The ProxyActions perform their * action not on the component they're bound to (menu items and * toolbar buttons), but on the component that currently * has the keyboard focus. Their enabled state tracks the * selection value of the component with the keyboard focus, * as well as the contents of the system clipboard. * </p> * <p> * The action code that reads and writes files, runs asynchronously * on background threads. The {@link #open open}, {@link #save save}, * and {@link #saveAs saveAs} actions all return a Task object which * encapsulates the work that will be done on a background thread. * The {@link #showAboutBox showAboutBox} and * {@link #closeAboutBox closeAboutBox} actions do their work * synchronously. * </p> * <p> * <strong>Warning:</strong> this application is intended as a simple * example, not as a robust text editor. Read it, don't use it. * * @see SingleFrameApplication * @see ResourceMap * @see Action * @see Task * @author Hans Muller (Hans.Muller@Sun.COM) */ public class DocumentExample extends SingleFrameApplication { private static final Logger logger = Logger.getLogger(DocumentExample.class.getName()); private JFileChooser fc; private JTextArea textArea; private File file = new File("untitled.txt"); private boolean modified = false; private TextMap texts = new TextMap(this); /* Class must be public for the properties to be injected */ @SuppressWarnings("serial") public class AppFrame extends MenuFrame { AppFrame() { super(DocumentExample.this); setConsoleVisible(false); // Create the main panel for this application. textArea = new JTextArea(); textArea.setName("textArea"); /* Poor man's dirty bit: if the document is ever edited, * assume that it needs to be saved. */ // This approach is not suitable for a real application. textArea.getDocument().addDocumentListener(new DocumentListener() { public void changedUpdate(DocumentEvent e) { setModified(true); } public void insertUpdate(DocumentEvent e) { setModified(true); } public void removeUpdate(DocumentEvent e) { setModified(true); } }); JScrollPane sp = new JScrollPane(textArea); sp.setPreferredSize(new Dimension(640, 480)); setMain(sp); // Menubar : injected // Toolbar : Injected // statusbar : default setTitle(getMainFrameTitle()); } } /** * Returns the texts. * * @return the texts */ public TextMap getTexts() { return texts; } /** * The File currently being edited. The default value of this * property is "untitled.txt". * <p> * This is a bound read-only property. It is never null. * * @return the value of the file property. * @see #isModified */ public File getFile() { return file; } /* Set the bound file property and update the GUI. */ private void setFile(File file) { File oldValue = this.file; this.file = file; getMainFrame().setTitle(getMainFrameTitle()); firePropertyChange("file", oldValue, this.file); } public String getMainFrameTitle() { String appId = getProperty(Application.TITLE_PROP); return file.getName() + " - " + appId; } /** * True if the file value has been modified but not saved. The * default value of this property is false. * <p> * This is a bound read-only property. * * @return the value of the modified property. * @see #isModified */ public boolean isModified() { return modified; } /* Set the bound modified property and update the GUI. */ private void setModified(boolean modified) { boolean oldValue = this.modified; this.modified = modified; firePropertyChange("modified", oldValue, this.modified); } private JFileChooser createFileChooser(String name) { fc.setName(name); String resName = "FileChooser(" + name + ")"; fc.setLocale(getLocale()); getContext().getResourceManager().inject(this, getLocale(), resName, fc); return fc; } /* A Task that loads the contents of a file into a String. The * LoadFileTask constructor runs first, on the EDT, then the * #doInBackground methods runs on a background thread, and finally * a completion method like #succeeded or #failed runs on the EDT. * * The resources for this class, like the message format strings are * loaded from resources/LoadFileTask.properties. */ private class LoadFileTask extends LoadTextFileTask { /* Construct the LoadFileTask object. The constructor * will run on the EDT, so we capture a reference to the * File to be loaded here. To keep things simple, the * resources for this Task are specified to be in the same * ResourceMap as the DocumentExample class's resources. * They're defined in resources/DocumentExample.properties. */ LoadFileTask(File file) { super(DocumentExample.this, file); } /* Called on the EDT if doInBackground completes without * error and this Task isn't cancelled. We update the * GUI as well as the file and modified properties here. */ @Override protected void succeeded(String fileContents) { setFile(getFile()); textArea.setText(fileContents); textArea.invalidate(); setModified(false); } /* Called on the EDT if doInBackground fails because * an uncaught exception is thrown. We show an error * dialog here. The dialog is configured with resources * loaded from this Tasks's ResourceMap. */ @Override protected void failed(Throwable e) { fileTaskfailed(getFile(), "load", e); } } protected void fileTaskfailed(File file, String taskName, Throwable e) { logger.log(Level.WARNING, "couldn't " + taskName + " " + file, e); String id = taskName + "Failed"; String message = texts.get(id, getFile(), e.getMessage()); showOption(getMainFrame(), id, message); } /** * Prompt the user for a filename and then attempt to load the file. * <p> * The file is loaded on a worker thread because we don't want to * block the EDT while the file system is accessed. To do that, * this Action method returns a new LoadFileTask instance, if the * user confirms selection of a file. The task is executed when * the "open" Action's actionPerformed method runs. The * LoadFileTask is responsible for updating the GUI after it has * successfully completed loading the file. * * @return a new LoadFileTask or null */ public Task<?,?> open() { JFileChooser fc = createFileChooser("open"); int option = fc.showOpenDialog(getMainFrame()); if (JFileChooser.APPROVE_OPTION == option) { return new LoadFileTask(fc.getSelectedFile()); } return null; } /* A Task that saves the contents of the textArea to the current file. * This class is very similar to LoadFileTask, please refer to that * class for more information. */ private class SaveFileTask extends SaveTextFileTask { SaveFileTask(File file) { super(DocumentExample.this, file, textArea.getText()); } @Override protected void succeeded(Void ignored) { setFile(getFile()); setModified(false); } @Override protected void failed(Throwable e) { fileTaskfailed(getFile(), "save", e); } } /** * Save the contents of the textArea to the current {@link #getFile file}. * <p> * The text is written to the file on a worker thread because we don't want to * block the EDT while the file system is accessed. To do that, this * Action method returns a new SaveFileTask instance. The task * is executed when the "save" Action's actionPerformed method runs. * The SaveFileTask is responsible for updating the GUI after it * has successfully completed saving the file. * * @see #getFile */ @Action(enabled = "modified") public Task<?, ?> save() { return new SaveFileTask(getFile()); } /** * Save the contents of the textArea to the current file. * <p> * This action is nearly identical to {@link #open open}. In * this case, if the user chooses a file, a {@code SaveFileTask} * is returned. Note that the selected file only becomes the * value of the {@code file} property if the file is saved * successfully. */ public Task<?,?> saveAs() { JFileChooser fc = createFileChooser("save"); int option = fc.showSaveDialog(getMainFrame()); if (JFileChooser.APPROVE_OPTION == option) { return new SaveFileTask(fc.getSelectedFile()); } return null; } /* Command line processing and initializations that need to * happen before the GUI is constructed should be done here. */ @Override protected void initialize(String[] args) { setLocale(Locale.ENGLISH); FileFilter fileFilter = new FileNameExtensionFilter(texts.get("txtExtDescription"), "txt"); fc = new JFileChooser(); fc.setLocale(getLocale()); fc.setFileFilter(fileFilter); } /* The GUI is created and made visible here. */ @Override protected void startup() { addApplicationListener(new ConfirmExit()); show(new FrameView(this, new AppFrame())); } /** * Launch the application on the EDT. * * @see Application#launch */ public static void main(String[] args) { Application app = new DocumentExample(); app.addApplicationListener(ApplicationListener.console); app.launch(args); } /** * A Task that saves a text String to a file. The file is not appended * to, its contents are replaced by the String. */ private static class SaveTextFileTask extends Task<Void, Void> { private final File file; private final String text; /** * Construct a SaveTextFileTask. * * @param file The file to save to * @param text The new contents of the file */ SaveTextFileTask(Application app, File file, String text) { this.file = file; this.text = text; } /** * Return the File that the {@link #getText text} will be * written to. * * @return the value of the read-only file property. */ public final File getFile() { return file; } private void renameFile(File oldFile, File newFile) throws IOException { if (!oldFile.renameTo(newFile)) { String fmt = "file rename failed: %s => %s"; throw new IOException(String.format(fmt, oldFile, newFile)); } } /** * Writes the {@code text} to the specified {@code file}. The * implementation is conservative: the {@code text} is initially * written to ${file}.tmp, then the original file is renamed * ${file}.bak, and finally the temporary file is renamed to ${file}. * The Task's {@code progress} property is updated as the text is * written. * <p> * If this Task is cancelled before writing the temporary file * has been completed, ${file.tmp} is deleted. * <p> * The conservative algorithm for saving to a file was lifted from * the FileSaver class described by Ian Darwin here: * <a href="http://javacook.darwinsys.com/new_recipes/10saveuserdata.jsp"> * http://javacook.darwinsys.com/new_recipes/10saveuserdata.jsp * </a>. * * @return null */ @Override protected Void doInBackground() throws IOException { String absPath = file.getAbsolutePath(); File tmpFile = new File(absPath + ".tmp"); tmpFile.createNewFile(); tmpFile.deleteOnExit(); File backupFile = new File(absPath + ".bak"); BufferedWriter out = null; int fileLength = text.length(); int blockSize = Math.max(1024, 1 + ((fileLength-1) / 100)); try { out = new BufferedWriter(new FileWriter(tmpFile)); int offset = 0; while(!isCancelled() && (offset < fileLength)) { int length = Math.min(blockSize, fileLength - offset); out.write(text, offset, length); offset += blockSize; setProgress(Math.min(offset, fileLength), 0, fileLength); } } finally { if (out != null) { out.close(); } } if (!isCancelled()) { backupFile.delete(); if (file.exists()) { renameFile(file, backupFile); } renameFile(tmpFile, file); } else { tmpFile.delete(); } return null; } } /** * A Task that loads the contents of a file into a String. */ private static class LoadTextFileTask extends Task<String, Void> { private final File file; /** * Construct a LoadTextFileTask. * * @param file the file to load from. */ LoadTextFileTask(Application application, File file) { this.file = file; } /** * Return the file being loaded. * * @return the value of the read-only file property. */ public final File getFile() { return file; } /** * Load the file into a String and return it. The * {@code progress} property is updated as the file is loaded. * <p> * If this task is cancelled before the entire file has been * read, null is returned. * * @return the contents of the {code file} as a String or null */ @Override protected String doInBackground() throws IOException { int fileLength = (int) file.length(); int nChars = -1; // progress updates after every blockSize chars read int blockSize = Math.max(1024, fileLength / 100); int p = blockSize; char[] buffer = new char[32]; StringBuilder contents = new StringBuilder(); BufferedReader rdr = new BufferedReader(new FileReader(file)); while (!isCancelled() && (nChars = rdr.read(buffer)) != -1) { contents.append(buffer, 0, nChars); if (contents.length() > p) { p += blockSize; setProgress(contents.length(), 0, fileLength); } } return !isCancelled() ? contents.toString() : null; } } private class ConfirmExit extends ApplicationListener.Adapter { public boolean canExit(EventObject e) { if (!isModified()) { // no edition work to save return true; } Object confirm = showOption(e, "confirmExit", texts.get("confirmExit", getFile())); return new Integer(JOptionPane.YES_OPTION).equals(confirm); } } }