source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/TextEditorPane.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 20.9 KB
Line 
1/*
2 * 11/25/2008
3 *
4 * TextEditorPane.java - A syntax highlighting text area that has knowledge of
5 * the file it is editing on disk.
6 *
7 * This library is distributed under a modified BSD license. See the included
8 * RSyntaxTextArea.License.txt file for details.
9 */
10package org.fife.ui.rsyntaxtextarea;
11
12import java.io.BufferedReader;
13import java.io.BufferedWriter;
14import java.io.File;
15import java.io.FileWriter;
16import java.io.IOException;
17import java.io.OutputStream;
18import java.io.PrintWriter;
19import java.nio.charset.Charset;
20import java.nio.charset.UnsupportedCharsetException;
21import javax.swing.event.DocumentEvent;
22import javax.swing.event.DocumentListener;
23import javax.swing.text.Document;
24
25import org.fife.io.UnicodeReader;
26import org.fife.io.UnicodeWriter;
27import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
28import org.fife.ui.rtextarea.RTextAreaEditorKit;
29
30
31/**
32 * An extension of {@link org.fife.ui.rsyntaxtextarea.RSyntaxTextArea}
33 * that adds information about the file being edited, such as:
34 *
35 * <ul>
36 * <li>Its name and location.
37 * <li>Is it dirty?
38 * <li>Is it read-only?
39 * <li>The last time it was loaded or saved to disk (local files only).
40 * <li>The file's encoding on disk.
41 * <li>Easy access to the line separator.
42 * </ul>
43 *
44 * Loading and saving is also built into the editor.<p>
45 * Both local and remote files (e.g. ftp) are supported. See the
46 * {@link FileLocation} class for more information.
47 *
48 * @author Robert Futrell
49 * @version 1.0
50 * @see FileLocation
51 */
52public class TextEditorPane extends RSyntaxTextArea implements
53 DocumentListener {
54
55 private static final long serialVersionUID = 1L;
56
57 public static final String FULL_PATH_PROPERTY = "TextEditorPane.fileFullPath";
58 public static final String DIRTY_PROPERTY = "TextEditorPane.dirty";
59 public static final String READ_ONLY_PROPERTY = "TextEditorPane.readOnly";
60
61 /**
62 * The location of the file being edited.
63 */
64 private FileLocation loc;
65
66 /**
67 * The charset to use when reading or writing this file.
68 */
69 private String charSet;
70
71 /**
72 * Whether the file should be treated as read-only.
73 */
74 private boolean readOnly;
75
76 /**
77 * Whether the file is dirty.
78 */
79 private boolean dirty;
80
81 /**
82 * The last time this file was modified on disk, for local files.
83 * For remote files, this value should always be
84 * {@link #LAST_MODIFIED_UNKNOWN}.
85 */
86 private long lastSaveOrLoadTime;
87
88 /**
89 * The value returned by {@link #getLastSaveOrLoadTime()} for remote files.
90 */
91 public static final long LAST_MODIFIED_UNKNOWN = 0;
92
93 /**
94 * The default name given to files if none is specified in a constructor.
95 */
96 private static final String DEFAULT_FILE_NAME = "Untitled.txt";
97
98
99 /**
100 * Constructor. The file will be given a default name.
101 */
102 public TextEditorPane() {
103 this(INSERT_MODE);
104 }
105
106
107 /**
108 * Constructor. The file will be given a default name.
109 *
110 * @param textMode Either <code>INSERT_MODE</code> or
111 * <code>OVERWRITE_MODE</code>.
112 */
113 public TextEditorPane(int textMode) {
114 this(textMode, false);
115 }
116
117
118 /**
119 * Creates a new <code>TextEditorPane</code>. The file will be given
120 * a default name.
121 *
122 * @param textMode Either <code>INSERT_MODE</code> or
123 * <code>OVERWRITE_MODE</code>.
124 * @param wordWrapEnabled Whether or not to use word wrap in this pane.
125 */
126 public TextEditorPane(int textMode, boolean wordWrapEnabled) {
127 super(textMode);
128 setLineWrap(wordWrapEnabled);
129 try {
130 init(null, null);
131 } catch (IOException ioe) { // Never happens
132 ioe.printStackTrace();
133 }
134 }
135
136
137 /**
138 * Creates a new <code>TextEditorPane</code>.
139 *
140 * @param textMode Either <code>INSERT_MODE</code> or
141 * <code>OVERWRITE_MODE</code>.
142 * @param wordWrapEnabled Whether or not to use word wrap in this pane.
143 * @param loc The location of the text file being edited. If this value
144 * is <code>null</code>, a file named "Untitled.txt" in the current
145 * directory is used.
146 * @throws IOException If an IO error occurs reading the file at
147 * <code>loc</code>. This of course won't happen if
148 * <code>loc</code> is <code>null</code>.
149 */
150 public TextEditorPane(int textMode, boolean wordWrapEnabled,
151 FileLocation loc) throws IOException {
152 this(textMode, wordWrapEnabled, loc, null);
153 }
154
155
156 /**
157 * Creates a new <code>TextEditorPane</code>.
158 *
159 * @param textMode Either <code>INSERT_MODE</code> or
160 * <code>OVERWRITE_MODE</code>.
161 * @param wordWrapEnabled Whether or not to use word wrap in this pane.
162 * @param loc The location of the text file being edited. If this value
163 * is <code>null</code>, a file named "Untitled.txt" in the current
164 * directory is used. This file is displayed as empty even if it
165 * actually exists.
166 * @param defaultEnc The default encoding to use when opening the file,
167 * if the file is not Unicode. If this value is <code>null</code>,
168 * a system default value is used.
169 * @throws IOException If an IO error occurs reading the file at
170 * <code>loc</code>. This of course won't happen if
171 * <code>loc</code> is <code>null</code>.
172 */
173 public TextEditorPane(int textMode, boolean wordWrapEnabled,
174 FileLocation loc, String defaultEnc) throws IOException {
175 super(textMode);
176 setLineWrap(wordWrapEnabled);
177 init(loc, defaultEnc);
178 }
179
180
181 /**
182 * Callback for when styles in the current document change.
183 * This method is never called.
184 *
185 * @param e The document event.
186 */
187 public void changedUpdate(DocumentEvent e) {
188 }
189
190
191
192 /**
193 * Returns the default encoding for this operating system.
194 *
195 * @return The default encoding.
196 */
197 private static final String getDefaultEncoding() {
198 // TODO: Change to "Charset.defaultCharset().name()" when 1.4 support
199 // is no longer needed.
200 // NOTE: The "file.encoding" property is not guaranteed to be set by
201 // the spec, so we cannot rely on it.
202 String encoding = System.getProperty("file.encoding");
203 if (encoding==null) {
204 try {
205 File f = File.createTempFile("rsta", null);
206 FileWriter w = new FileWriter(f);
207 encoding = w.getEncoding();
208 w.close();
209 f.deleteOnExit();//delete(); Keep FindBugs happy
210 } catch (IOException ioe) {
211 encoding = "US-ASCII";
212 }
213 }
214 return encoding;
215 }
216
217
218 /**
219 * Returns the encoding to use when reading or writing this file.
220 *
221 * @return The encoding.
222 * @see #setEncoding(String)
223 */
224 public String getEncoding() {
225 return charSet;
226 }
227
228
229 /**
230 * Returns the full path to this document.
231 *
232 * @return The full path to the document.
233 */
234 public String getFileFullPath() {
235 return loc==null ? null : loc.getFileFullPath();
236 }
237
238
239 /**
240 * Returns the file name of this document.
241 *
242 * @return The file name.
243 */
244 public String getFileName() {
245 return loc.getFileName();
246 }
247
248
249 /**
250 * Returns the timestamp for when this file was last loaded or saved
251 * <em>by this editor pane</em>. If the file has been modified on disk by
252 * another process after it was loaded into this editor pane, this method
253 * will not return the actual file's last modified time.<p>
254 *
255 * For remote files, this method will always return
256 * {@link #LAST_MODIFIED_UNKNOWN}.
257 *
258 * @return The timestamp when this file was last loaded or saved by this
259 * editor pane, if it is a local file, or
260 * {@link #LAST_MODIFIED_UNKNOWN} if it is a remote file.
261 * @see #isModifiedOutsideEditor()
262 */
263 public long getLastSaveOrLoadTime() {
264 return lastSaveOrLoadTime;
265 }
266
267
268 /**
269 * Returns the line separator used when writing this file (e.g.
270 * "<code>\n</code>", "<code>\r\n</code>", or "<code>\r</code>").<p>
271 *
272 * Note that this value is an <code>Object</code> and not a
273 * <code>String</code> as that is the way the {@link Document} interface
274 * defines its property values. If you always use
275 * {@link #setLineSeparator(String)} to modify this value, then the value
276 * returned from this method will always be a <code>String</code>.
277 *
278 * @return The line separator. If this value is <code>null</code>, then
279 * the system default line separator is used (usually the value
280 * of <code>System.getProperty("line.separator")</code>).
281 * @see #setLineSeparator(String)
282 * @see #setLineSeparator(String, boolean)
283 */
284 public Object getLineSeparator() {
285 return getDocument().getProperty(
286 RTextAreaEditorKit.EndOfLineStringProperty);
287 }
288
289
290 /**
291 * Initializes this editor with the specified file location.
292 *
293 * @param loc The file location. If this is <code>null</code>, a default
294 * location is used and an empty file is displayed.
295 * @param defaultEnc The default encoding to use when opening the file,
296 * if the file is not Unicode. If this value is <code>null</code>,
297 * a system default value is used.
298 * @throws IOException If an IO error occurs reading from <code>loc</code>.
299 * If <code>loc</code> is <code>null</code>, this cannot happen.
300 */
301 private void init(FileLocation loc, String defaultEnc) throws IOException {
302
303 if (loc==null) {
304 // Don't call load() just in case Untitled.txt actually exists,
305 // just to ensure there is no chance of an IOException being thrown
306 // in the default case.
307 this.loc = FileLocation.create(DEFAULT_FILE_NAME);
308 charSet = defaultEnc==null ? getDefaultEncoding() : defaultEnc;
309 // Ensure that line separator always has a value, even if the file
310 // does not exist (or is the "default" file). This makes life
311 // easier for host applications that want to display this value.
312 setLineSeparator(System.getProperty("line.separator"));
313 }
314 else {
315 load(loc, defaultEnc); // Sets this.loc
316 }
317
318 if (this.loc.isLocalAndExists()) {
319 File file = new File(this.loc.getFileFullPath());
320 lastSaveOrLoadTime = file.lastModified();
321 setReadOnly(!file.canWrite());
322 }
323 else {
324 lastSaveOrLoadTime = LAST_MODIFIED_UNKNOWN;
325 setReadOnly(false);
326 }
327
328 setDirty(false);
329
330 }
331
332
333 /**
334 * Callback for when text is inserted into the document.
335 *
336 * @param e Information on the insertion.
337 */
338 public void insertUpdate(DocumentEvent e) {
339 if (!dirty) {
340 setDirty(true);
341 }
342 }
343
344
345 /**
346 * Returns whether or not the text in this editor has unsaved changes.
347 *
348 * @return Whether or not the text has unsaved changes.
349 */
350 public boolean isDirty() {
351 return dirty;
352 }
353
354
355 /**
356 * Returns whether this file is a local file.
357 *
358 * @return Whether this is a local file.
359 */
360 public boolean isLocal() {
361 return loc.isLocal();
362 }
363
364
365 /**
366 * Returns whether this is a local file that already exists.
367 *
368 * @return Whether this is a local file that already exists.
369 */
370 public boolean isLocalAndExists() {
371 return loc.isLocalAndExists();
372 }
373
374
375 /**
376 * Returns whether the text file has been modified outside of this editor
377 * since the last load or save operation. Note that if this is a remote
378 * file, this method will always return <code>false</code>.<p>
379 *
380 * This method may be used by applications to implement a reloading
381 * feature, where the user is prompted to reload a file if it has been
382 * modified since their last open or save.
383 *
384 * @return Whether the text file has been modified outside of this
385 * editor.
386 * @see #getLastSaveOrLoadTime()
387 */
388 public boolean isModifiedOutsideEditor() {
389 return loc.getActualLastModified()>getLastSaveOrLoadTime();
390 }
391
392
393 /**
394 * Returns whether or not the text area should be treated as read-only.
395 *
396 * @return Whether or not the text area should be treated as read-only.
397 * @see #setReadOnly(boolean)
398 */
399 public boolean isReadOnly() {
400 return readOnly;
401 }
402
403
404 /**
405 * Loads the specified file in this editor. This method fires a property
406 * change event of type {@link #FULL_PATH_PROPERTY}.
407 *
408 * @param loc The location of the file to load. This cannot be
409 * <code>null</code>.
410 * @param defaultEnc The encoding to use when loading/saving the file.
411 * This encoding will only be used if the file is not Unicode.
412 * If this value is <code>null</code>, the system default encoding
413 * is used.
414 * @throws IOException If an IO error occurs.
415 * @see #save()
416 * @see #saveAs(FileLocation)
417 */
418 public void load(FileLocation loc, String defaultEnc) throws IOException {
419
420 // For new local files, just go with it.
421 if (loc.isLocal() && !loc.isLocalAndExists()) {
422 this.charSet = defaultEnc!=null ? defaultEnc : getDefaultEncoding();
423 this.loc = loc;
424 return;
425 }
426
427 // Old local files and remote files, load 'em up. UnicodeReader will
428 // check for BOMs and handle them correctly in all cases, then pass
429 // rest of stream down to InputStreamReader.
430 UnicodeReader ur = new UnicodeReader(loc.getInputStream(), defaultEnc);
431
432 // Remove listener so dirty flag doesn't get set when loading a file.
433 Document doc = getDocument();
434 doc.removeDocumentListener(this);
435 BufferedReader r = new BufferedReader(ur);
436 try {
437 read(r, null);
438 } finally {
439 doc.addDocumentListener(this);
440 r.close();
441 }
442
443 // No IOException thrown, so we can finally change the location.
444 charSet = ur.getEncoding();
445 String old = getFileFullPath();
446 this.loc = loc;
447 firePropertyChange(FULL_PATH_PROPERTY, old, getFileFullPath());
448
449 }
450
451
452 /**
453 * Reloads this file from disk. The file must exist for this operation
454 * to not throw an exception.<p>
455 *
456 * The file's "dirty" state will be set to <code>false</code> after this
457 * operation. If this is a local file, its "last modified" time is
458 * updated to reflect that of the actual file.<p>
459 *
460 * Note that if the file has been modified on disk, and is now a Unicode
461 * encoding when before it wasn't (or if it is a different Unicode now),
462 * this will cause this {@link TextEditorPane}'s encoding to change.
463 * Otherwise, the file's encoding will stay the same.
464 *
465 * @throws IOException If the file does not exist, or if an IO error
466 * occurs reading the file.
467 * @see #isLocalAndExists()
468 */
469 public void reload() throws IOException {
470 String oldEncoding = getEncoding();
471 UnicodeReader ur = new UnicodeReader(loc.getInputStream(), oldEncoding);
472 String encoding = ur.getEncoding();
473 BufferedReader r = new BufferedReader(ur);
474 try {
475 read(r, null); // Dumps old contents.
476 } finally {
477 r.close();
478 }
479 setEncoding(encoding);
480 setDirty(false);
481 syncLastSaveOrLoadTimeToActualFile();
482 discardAllEdits(); // Prevent user from being able to undo the reload
483 }
484
485
486 /**
487 * Called whenever text is removed from this editor.
488 *
489 * @param e The document event.
490 */
491 public void removeUpdate(DocumentEvent e) {
492 if (!dirty) {
493 setDirty(true);
494 }
495 }
496
497
498 /**
499 * Saves the file in its current encoding.<p>
500 *
501 * The text area's "dirty" state is set to <code>false</code>, and if
502 * this is a local file, its "last modified" time is updated.
503 *
504 * @throws IOException If an IO error occurs.
505 * @see #saveAs(FileLocation)
506 * @see #load(FileLocation, String)
507 */
508 public void save() throws IOException {
509 saveImpl(loc);
510 setDirty(false);
511 syncLastSaveOrLoadTimeToActualFile();
512 }
513
514
515 /**
516 * Saves this file in a new local location. This method fires a property
517 * change event of type {@link #FULL_PATH_PROPERTY}.
518 *
519 * @param loc The location to save to.
520 * @throws IOException If an IO error occurs.
521 * @see #save()
522 * @see #load(FileLocation, String)
523 */
524 public void saveAs(FileLocation loc) throws IOException {
525 saveImpl(loc);
526 // No exception thrown - we can "rename" the file.
527 String old = getFileFullPath();
528 this.loc = loc;
529 setDirty(false);
530 lastSaveOrLoadTime = loc.getActualLastModified();
531 firePropertyChange(FULL_PATH_PROPERTY, old, getFileFullPath());
532 }
533
534
535 /**
536 * Saves the text in this editor to the specified location.
537 *
538 * @param loc The location to save to.
539 * @throws IOException If an IO error occurs.
540 */
541 private void saveImpl(FileLocation loc) throws IOException {
542 OutputStream out = loc.getOutputStream();
543 PrintWriter w = new PrintWriter(
544 new BufferedWriter(new UnicodeWriter(out, getEncoding())));
545 try {
546 write(w);
547 } finally {
548 w.close();
549 }
550 }
551
552
553 /**
554 * Sets whether or not this text in this editor has unsaved changes.
555 * This fires a property change event of type {@link #DIRTY_PROPERTY}.
556 *
557 * @param dirty Whether or not the text has been modified.
558 * @see #isDirty()
559 */
560 private void setDirty(boolean dirty) {
561 if (this.dirty!=dirty) {
562 this.dirty = dirty;
563 firePropertyChange(DIRTY_PROPERTY, !dirty, dirty);
564 }
565 }
566
567
568 /**
569 * Sets the document for this editor.
570 *
571 * @param doc The new document.
572 */
573 public void setDocument(Document doc) {
574 Document old = getDocument();
575 if (old!=null) {
576 old.removeDocumentListener(this);
577 }
578 super.setDocument(doc);
579 doc.addDocumentListener(this);
580 }
581
582
583 /**
584 * Sets the encoding to use when reading or writing this file. This
585 * method sets the editor's dirty flag when the encoding is changed.
586 *
587 * @param encoding The new encoding.
588 * @throws UnsupportedCharsetException If the encoding is not supported.
589 * @throws NullPointerException If <code>encoding</code> is
590 * <code>null</code>.
591 * @see #getEncoding()
592 */
593 public void setEncoding(String encoding) {
594 if (encoding==null) {
595 throw new NullPointerException("encoding cannot be null");
596 }
597 else if (!Charset.isSupported(encoding)) {
598 throw new UnsupportedCharsetException(encoding);
599 }
600 if (charSet==null || !charSet.equals(encoding)) {
601 charSet = encoding;
602 setDirty(true);
603 }
604 }
605
606
607 /**
608 * Sets the line separator sequence to use when this file is saved (e.g.
609 * "<code>\n</code>", "<code>\r\n</code>" or "<code>\r</code>").
610 *
611 * Besides parameter checking, this method is preferred over
612 * <code>getDocument().putProperty()</code> because it sets the editor's
613 * dirty flag when the line separator is changed.
614 *
615 * @param separator The new line separator.
616 * @throws NullPointerException If <code>separator</code> is
617 * <code>null</code>.
618 * @throws IllegalArgumentException If <code>separator</code> is not one
619 * of "<code>\n</code>", "<code>\r\n</code>" or "<code>\r</code>".
620 * @see #getLineSeparator()
621 */
622 public void setLineSeparator(String separator) {
623 setLineSeparator(separator, true);
624 }
625
626
627 /**
628 * Sets the line separator sequence to use when this file is saved (e.g.
629 * "<code>\n</code>", "<code>\r\n</code>" or "<code>\r</code>").
630 *
631 * Besides parameter checking, this method is preferred over
632 * <code>getDocument().putProperty()</code> because can set the editor's
633 * dirty flag when the line separator is changed.
634 *
635 * @param separator The new line separator.
636 * @param setDirty Whether the dirty flag should be set if the line
637 * separator is changed.
638 * @throws NullPointerException If <code>separator</code> is
639 * <code>null</code>.
640 * @throws IllegalArgumentException If <code>separator</code> is not one
641 * of "<code>\n</code>", "<code>\r\n</code>" or "<code>\r</code>".
642 * @see #getLineSeparator()
643 */
644 public void setLineSeparator(String separator, boolean setDirty) {
645 if (separator==null) {
646 throw new NullPointerException("terminator cannot be null");
647 }
648 if (!"\r\n".equals(separator) && !"\n".equals(separator) &&
649 !"\r".equals(separator)) {
650 throw new IllegalArgumentException("Invalid line terminator");
651 }
652 Document doc = getDocument();
653 Object old = doc.getProperty(
654 RTextAreaEditorKit.EndOfLineStringProperty);
655 if (!separator.equals(old)) {
656 doc.putProperty(RTextAreaEditorKit.EndOfLineStringProperty,
657 separator);
658 if (setDirty) {
659 setDirty(true);
660 }
661 }
662 }
663
664
665 /**
666 * Sets whether or not this text area should be treated as read-only.
667 * This fires a property change event of type {@link #READ_ONLY_PROPERTY}.
668 *
669 * @param readOnly Whether or not the document is read-only.
670 * @see #isReadOnly()
671 */
672 public void setReadOnly(boolean readOnly) {
673 if (this.readOnly!=readOnly) {
674 this.readOnly = readOnly;
675 firePropertyChange(READ_ONLY_PROPERTY, !readOnly, readOnly);
676 }
677 }
678
679
680 /**
681 * Syncs this text area's "last saved or loaded" time to that of the file
682 * being edited, if that file is local and exists. If the file is
683 * remote or is local but does not yet exist, nothing happens.<p>
684 *
685 * You normally do not have to call this method, as the "last saved or
686 * loaded" time for {@link TextEditorPane}s is kept up-to-date internally
687 * during such operations as {@link #save()}, {@link #reload()}, etc.
688 *
689 * @see #getLastSaveOrLoadTime()
690 * @see #isModifiedOutsideEditor()
691 */
692 public void syncLastSaveOrLoadTimeToActualFile() {
693 if (loc.isLocalAndExists()) {
694 lastSaveOrLoadTime = loc.getActualLastModified();
695 }
696 }
697
698
699}
Note: See TracBrowser for help on using the repository browser.