Source code: com/memoire/bu/BuTextArea.java
1 /**
2 * @modification $Date: 2002/11/14 19:25:32 $
3 * @statut unstable
4 * @file BuTextArea.java
5 * @version 0.36
6 * @author Romain Guy
7 * @email guy.romain@bigfoot.com
8 * @license GNU General Public License 2 (GPL2)
9 * @copyright 1998-2001 Guillaume Desnoix
10 */
11 package com.memoire.bu;
12 import com.memoire.bu.*;
13 import com.memoire.dnd.*;
14 import com.memoire.fu.*;
15 import com.memoire.re.*;
16
17
18 /*
19 * 19:13:20 13/06/99
20 *
21 * JextTextAreaLite.java - A 'lite' extended JTextArea
22 * Copyright (C) 1998-1999 Romain Guy
23 * Especially designed for Nicolas Warneck
24 * powerteam@chez.com
25 * www.chez.com/powerteam
26 *
27 * This program is free software; you can redistribute it and/or
28 * modify it under the terms of the GNU General Public License
29 * as published by the Free Software Foundation; either version 2
30 * of the License, or any later version.
31 *
32 * This program is distributed in the hope that it will be useful,
33 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35 * GNU General Public License for more details.
36 *
37 * You should have received a copy of the GNU General Public License
38 * along with this program; if not, write to the Free Software
39 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
40 *
41 *
42 * A few changes and adaptations 25/5/2000 (C) Guillaume Desnoix
43 */
44
45 import java.io.*;
46 import java.awt.*;
47 // import gnu.regexp.*;
48 import java.util.zip.*;
49 import java.awt.event.*;
50
51 import javax.swing.*;
52 import javax.swing.undo.*;
53 import javax.swing.event.*;
54 import javax.swing.text.*;
55
56 /**
57 * This component extends a JTextArea, providing methods to
58 * undo/redo, find/replace and read/save.
59 *
60 * This component extends a JTextArea. This component provides
61 * its own methods to read and save files (even to zip them).
62 * @author Romain Guy
63 * @email guy.romain@bigfoot.com
64 * @version 1.1
65 */
66 public class BuTextArea extends JTextArea
67 implements UndoableEditListener, DocumentListener,
68 BuTextComponentInterface
69 {
70 /** This constant defines an open dialog box. */
71 public static final int OPEN = 0;
72 /** This constant defines a save dialog box. */
73 public static final int SAVE = 1;
74
75 // Private declaration
76
77 private RE regexp;
78 private CompoundEdit compoundEdit;
79 private String fontName, currentFile;
80 private int anchor, fontSize, fontStyle;
81 private boolean dirty, newText, operation;
82 private UndoManager undo = new UndoManager();
83
84 /** This constant defines the size of the buffer used to read files */
85 private static final int BUFFER_SIZE = 32768;
86
87 /**
88 * The constructor add the necessary listeners, set some stuffs
89 * (caret color, borers, fonts...).
90 */
91
92 public BuTextArea()
93 {
94 this(15,40);
95 }
96
97 public BuTextArea(int _lines, int _cols)
98 {
99 super(_lines, _cols);
100
101 // setBorder(null);
102 // setCaretColor(Color.red);
103
104 getDocument().addDocumentListener(this);
105 getDocument().addUndoableEditListener(this);
106
107 Font defaultFont = getFont(); // new Font("Monospaced", Font.PLAIN, 12);
108 fontName = defaultFont.getName();
109 fontSize = defaultFont.getSize();
110 fontStyle = defaultFont.getStyle();
111 // setFont(defaultFont);
112 }
113
114 // Anti-aliasing
115
116 public void paint(Graphics _g)
117 {
118 BuLib.setAntialiasing(_g);
119 super.paint(_g);
120 }
121
122 public void select()
123 {
124 select(0,getLength());
125 }
126
127 public void duplicate()
128 {
129 int s=getSelectionStart();
130 int e=getSelectionEnd();
131 if(s<e)
132 {
133 copy();
134 setCaretPosition(e);
135 paste();
136 setSelectionEnd(e+(e-s));
137 setSelectionStart(e);
138 }
139 }
140
141 public void go(int _line)
142 {
143 Element map =getDocument().getDefaultRootElement();
144 Element line=map.getElement(_line);
145 select(line.getStartOffset(),line.getEndOffset());
146 }
147
148 /**
149 * Display a file chooser dialog box.
150 * @param owner <code>Component</code> which 'owns' the dialog
151 * @param mode Can be either <code>LOAD</code> or <code>SAVE</code>
152 * @return The path to selected file, null otherwise
153 */
154
155 public static String chooseFile(Component owner, int mode)
156 {
157 BuFileChooser chooser = new BuFileChooser();
158 if (mode == OPEN)
159 chooser.setDialogType(BuFileChooser.OPEN_DIALOG);
160 else if (mode == SAVE)
161 chooser.setDialogType(BuFileChooser.SAVE_DIALOG);
162 chooser.setFileSelectionMode(BuFileChooser.FILES_ONLY);
163 chooser.setFileHidingEnabled(true);
164 if (chooser.showDialog(owner, null) == BuFileChooser.APPROVE_OPTION)
165 return chooser.getSelectedFile().getAbsolutePath();
166 return null;
167 }
168
169 /**
170 * Display a sample message in a dialog box.
171 * @param message The message to display
172 */
173
174 public static void showMessage(String message)
175 {
176 JOptionPane.showMessageDialog(null, message, "Message", JOptionPane.INFORMATION_MESSAGE);
177 }
178
179 /**
180 * Display an error message in a dialog box.
181 * @param message The message to display
182 */
183
184 public static void showError(String message)
185 {
186 JOptionPane.showMessageDialog(null,message,"Error",
187 JOptionPane.ERROR_MESSAGE);
188 }
189 /**
190 * Return current font's name
191 */
192
193 public String getFontName()
194 {
195 return fontName;
196 }
197
198 /**
199 * Return current font's size
200 */
201
202 public int getFontSize()
203 {
204 return fontSize;
205 }
206
207 /**
208 * Return current font's style (bold, italic...)
209 */
210
211 public int getFontStyle()
212 {
213 return fontStyle;
214 }
215
216 /**
217 * Set the font which has to be used.
218 * @param name The name of the font
219 */
220
221 public void setFontName(String name)
222 {
223 fontName = name;
224 changeFont();
225 }
226
227 /**
228 * Set the size of the font.
229 * @param size The new font's size
230 */
231
232 public void setFontSize(int size)
233 {
234 fontSize = size;
235 changeFont();
236 repaint();
237 }
238
239 /**
240 * Set the style of the font.
241 * @param style The new style to apply
242 */
243
244 public void setFontStyle(int style)
245 {
246 fontStyle = style;
247 changeFont();
248 repaint();
249 }
250
251 /**
252 * Set the new font.
253 */
254
255 private void changeFont()
256 {
257 setFont(new Font(fontName, fontStyle, fontSize));
258 }
259
260 /**
261 * Return the full path of the opened file.
262 */
263
264 public String getCurrentFile()
265 {
266 return currentFile;
267 }
268
269 /**
270 * When an operation has began, setChanged() cannot be called.
271 * This is very important when we need to insert or remove some
272 * parts of the texte without turning on the 'to_be_saved' flag.
273 */
274
275 public void beginOperation()
276 {
277 operation = true;
278 }
279
280 /**
281 * Calling this will allow the DocumentListener to use setChanged().
282 */
283
284 public void endOperation()
285 {
286 operation = false;
287 }
288
289 /**
290 * Return true if we can use the setChanged() method,
291 * false otherwise.
292 */
293
294 public boolean getOperation()
295 {
296 return operation;
297 }
298
299 /**
300 * Set a new file. We first ask the user if he'd like to save its
301 * changes (if some have been made).
302 */
303
304 public void newFile()
305 {
306 beginOperation(); // we don't want to see a 'modified' message
307 if (isDirty() && !isEmpty())
308 {
309 int response = JOptionPane.showConfirmDialog(null, "Do you want to save your changes ?", "Save", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
310 switch (response)
311 {
312 case 0:
313 saveContent();
314 break;
315 case 1:
316 break;
317 case 2:
318 endOperation();
319 return;
320 default:
321 endOperation();
322 return;
323 }
324 }
325 // we have to create a new document, so we remove
326 // old listeners and create new ones below
327 getDocument().removeUndoableEditListener(this);
328 getDocument().removeDocumentListener(this);
329 clean();
330 newText = true;
331 currentFile = null;
332 setEditable(true);
333 setText("");
334 setAnchor(0);
335 discard();
336 getDocument().addUndoableEditListener(this);
337 getDocument().addDocumentListener(this);
338 endOperation();
339 }
340
341 /**
342 * This is just to reduce code size of other classes.
343 * @param off The line index
344 * @return The offset in the text where the line begins
345 */
346
347 public int getLineStartOffset(int off)
348 {
349 return getDocument().getDefaultRootElement().getElement(off).getStartOffset();
350 }
351
352 /**
353 * Called to save current content in specified zip file.
354 * Call zip(String file) but asks user for overwriting if
355 * file already exists.
356 */
357
358 public void zipContent()
359 {
360 if (getText().equals("")) return;
361 String zipFile = chooseFile(this, SAVE);
362 if (zipFile != null)
363 {
364 if (!zipFile.endsWith(".zip")) zipFile += ".zip";
365 if (!(new File(zipFile)).exists())
366 zip(zipFile);
367 else
368 {
369 int response = JOptionPane.showConfirmDialog(null, "File already exists, overwrite it ?", "Save", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
370 switch (response)
371 {
372 case 0:
373 zip(zipFile);
374 break;
375 case 1:
376 break;
377 default:
378 return;
379 }
380 }
381 }
382 }
383
384 /**
385 * Zip text area content into specified file.
386 * @param zipFile The file name where to zip the text
387 */
388
389 public void zip(String zipFile)
390 {
391 try
392 {
393 ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));
394 out.putNextEntry(new ZipEntry((new File(currentFile)).getName()));
395 // we ensure we use system's carriage return char
396 String newline = System.getProperty("line.separator");
397 Element map = getDocument().getDefaultRootElement();
398 // we zip the text line by line
399 for (int i = 0; i < map.getElementCount(); i++)
400 {
401 Element line = map.getElement(i);
402 int start = line.getStartOffset();
403 byte[] buf = (getText(start, line.getEndOffset() - start - 1) + newline).getBytes();
404 out.write(buf, 0, buf.length);
405 }
406 out.closeEntry();
407 out.close();
408 } catch(IOException ioe) {
409 showError("Error has occured while ziping");
410 } catch(BadLocationException ble) {
411 showError("Error has occured while ziping");
412 }
413 }
414
415 /**
416 * Called to save this component's content.
417 * Call save(String file) but let the user choosing a file name.
418 * In the case the user choosed an existing file, we ask him if
419 * he really wants to overwrite it.
420 */
421
422 public void saveContent()
423 {
424 String fileToSave = chooseFile(this, SAVE);
425 if (fileToSave != null)
426 {
427 if (!(new File(fileToSave)).exists())
428 save(fileToSave);
429 else
430 {
431 int response = JOptionPane.showConfirmDialog(null, "File already exists, overwrite it ?", "Save", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
432 switch (response)
433 {
434 case 0:
435 save(fileToSave);
436 break;
437 case 1:
438 break;
439 default:
440 return;
441 }
442 }
443 }
444 }
445
446 /**
447 * Store the text in a specified file.
448 * @param file The file in wich we'll write the text
449 */
450
451 public void save(String file)
452 {
453 try
454 {
455 OutputStream outs = new FileOutputStream(new File(file));
456 BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outs), BUFFER_SIZE);
457 // we ensure we use system's carriage return char
458 String newline = System.getProperty("line.separator");
459 Element map = getDocument().getDefaultRootElement();
460 // we write the file line by line
461 for (int i = 0; i < map.getElementCount(); i++)
462 {
463 Element line = map.getElement(i);
464 int start = line.getStartOffset();
465 out.write(getText(start, line.getEndOffset() - start - 1));
466 out.write(newline);
467 }
468 out.close();
469 outs.close();
470 if (!file.equals(currentFile)) currentFile = file;
471 } catch (IOException ioe) {
472 showError("Error has occured while saving");
473 } catch (BadLocationException ble) {
474 showError("Error has occured while saving");
475 }
476 }
477
478 /**
479 * Called to load a new file in the text area.
480 * Determines which line separator (\n, \r\n...) are used in the
481 * file to open. Convert'em into Swing line separator (\n).
482 * @param path The path of the file to be loaded
483 */
484
485 public void open(String path)
486 {
487 beginOperation();
488 // we do the same thing as in newFile() for the listeners
489 getDocument().removeUndoableEditListener(this);
490 getDocument().removeDocumentListener(this);
491 clean();
492 discard();
493 InputStream is;
494 StringBuffer buffer = new StringBuffer();
495 try
496 {
497 File toLoad = new File(path);
498 // we check if the file is read only or not
499 if (!toLoad.canWrite())
500 setEditable(false);
501 else if (!isEditable())
502 setEditable(true);
503 is = new FileInputStream(toLoad);
504 InputStreamReader in = new InputStreamReader(is);
505 char[] buf = new char[BUFFER_SIZE];
506 int len;
507 int lineCount = 0;
508 boolean CRLF = false;
509 boolean CROnly = false;
510 boolean lastWasCR = false;
511
512 // we read the file until the end of it (amazing, hu ?)
513 while ((len = in.read(buf, 0, buf.length)) != -1)
514 {
515 int lastLine = 0;
516 for (int i = 0; i < len; i++)
517 {
518 switch(buf[i])
519 {
520 // and we convert system's carriage return char into \n
521 case '\r':
522 if (lastWasCR)
523 {
524 CROnly = true;
525 CRLF = false;
526 } else
527 lastWasCR = true;
528 buffer.append(buf, lastLine, i - lastLine);
529 buffer.append('\n');
530 lastLine = i + 1;
531 break;
532 case '\n':
533 if (lastWasCR)
534 {
535 CROnly = false;
536 CRLF = true;
537 lastWasCR = false;
538 lastLine = i + 1;
539 } else {
540 CROnly = false;
541 CRLF = false;
542 buffer.append(buf, lastLine, i - lastLine);
543 buffer.append('\n');
544 lastLine = i + 1;
545 }
546 break;
547 default:
548 if (lastWasCR)
549 {
550 CROnly = true;
551 CRLF = false;
552 lastWasCR = false;
553 }
554 break;
555 }
556 }
557 buffer.append(buf, lastLine, len - lastLine);
558 }
559 in.close();
560 if (buffer.length() != 0 && buffer.charAt(buffer.length() - 1) == '\n')
561 buffer.setLength(buffer.length() - 1);
562
563 // we clear the area
564 getDocument().remove(0, getLength());
565 // we put the text in it
566 getDocument().insertString(0, buffer.toString(), null);
567 currentFile = path;
568 setCaretPosition(0);
569 newText = false;
570 getDocument().addUndoableEditListener(this);
571 getDocument().addDocumentListener(this);
572 } catch(BadLocationException bl) {
573 bl.printStackTrace();
574 } catch(FileNotFoundException fnf) {
575 showError(path + " not found !");
576 } catch(IOException io) {
577 showError(io.toString());
578 }
579 endOperation();
580 }
581
582 /**
583 * Return true if current text is new, false otherwise.
584 */
585
586 public boolean isNew()
587 {
588 return newText;
589 }
590
591 /**
592 * Return true if area is empty, false otherwise.
593 */
594
595 public boolean isEmpty()
596 {
597 if (getLength() == 0)
598 return true;
599 else
600 return false;
601 }
602
603
604 /**
605 * Return true if area content has changed, false otherwise.
606 */
607
608 public boolean isDirty()
609 {
610 return dirty;
611 }
612
613 /**
614 * Called when the content of the area has changed.
615 */
616
617 public void setDirty()
618 {
619 dirty = true;
620 }
621
622 /**
623 * Called after having saved or created a new document to ensure
624 * the content isn't 'dirty'.
625 */
626
627 public void clean()
628 {
629 dirty = false;
630 }
631
632 /**
633 * Discard all edits contained in the UndoManager.
634 */
635
636 public void discard()
637 {
638 undo.discardAllEdits();
639 }
640
641 /**
642 * Useful for the GUI.
643 */
644 public UndoManager getUndo()
645 {
646 return undo;
647 }
648
649 /**
650 * undo the last operation
651 */
652 public void undo()
653 {
654 // System.err.println("==== UNDO");
655 if(undo.canUndo()) undo.undo();
656 }
657
658 /**
659 * redo the last operation
660 */
661 public void redo()
662 {
663 // System.err.println("==== REDO");
664 if(undo.canRedo()) undo.redo();
665 }
666
667 /**
668 * Return the anchor position.
669 */
670
671 public int getAnchor()
672 {
673 return anchor;
674 }
675
676 /**
677 * Set the anchor postion.
678 * @param offset The new anchor's position
679 */
680
681 public void setAnchor(int offset)
682 {
683 anchor = offset;
684 }
685
686 /**
687 * Return the lentgh of the text in the area.
688 */
689
690 public int getLength()
691 {
692 Document doc=getDocument();
693 return (doc!=null) ? doc.getLength() : 0;
694 }
695
696 /**
697 * Used for ReplaceAll.
698 * This merges all text changes made between the beginCompoundEdit()
699 * and the endCompoundEdit() calls into only one undo event.
700 */
701
702 public void beginCompoundEdit()
703 {
704 if (compoundEdit == null)
705 compoundEdit = new CompoundEdit();
706 }
707
708 /**
709 * See beginCompoundEdit().
710 */
711
712 public void endCompoundEdit()
713 {
714 if (compoundEdit != null)
715 {
716 compoundEdit.end();
717 if (compoundEdit.canUndo())
718 undo.addEdit(compoundEdit);
719 compoundEdit = null;
720 }
721 }
722
723 /**
724 * Return the result of a string search.
725 * @param searchStr The string to be found
726 * @param start The search's start offset
727 * @param ignoreCase Set to true, we'll ignore the text case
728 * @return True if <code>searchStr</code> has been found, false otherwise
729 */
730
731 public boolean find(String searchStr, int start, boolean ignoreCase)
732 {
733 try
734 {
735 if (searchStr.equals("") || searchStr == null) return false;
736 regexp = new RE(searchStr, (ignoreCase == true ? RE.REG_ICASE : 0) | RE.REG_MULTILINE, RESyntax.RE_SYNTAX_PERL5);
737 if (regexp == null)
738 {
739 getToolkit().beep();
740 return false;
741 }
742 String text = getText(start, getLength() - start);
743 REMatch match = regexp.getMatch(text);
744 if (match != null)
745 {
746 select(start + match.getStartIndex(), start + match.getEndIndex());
747 return true;
748 }
749 } catch(Exception e) { }
750 return false;
751 }
752
753 public boolean replace(String searchStr, String replaceStr,
754 int start, int end, boolean ignoreCase)
755 {
756 boolean r=false;
757 try { r=replaceAll(searchStr,replaceStr,start,end,ignoreCase); }
758 catch(Exception ex) { }
759 return r;
760 }
761
762 /**
763 * Return the result of a string replace.
764 * @param searchStr The string to be found
765 * @param replaceStr The string which will replace <code>searchStr</code>
766 * @param start The search's start offset
767 * @param end The search's end offset
768 * @param ignoreCase Set to true, we'll ignore the text case
769 * @return True if the replace has been successfully done, false otherwise
770 */
771
772 public boolean replaceAll(String searchStr, String replaceStr, int start, int end, boolean ignoreCase) throws REException
773 {
774 boolean found = false;
775 beginCompoundEdit();
776 try
777 {
778 if (searchStr.equals("") || searchStr == null) return false;
779 regexp = new RE(searchStr, (ignoreCase == true ? RE.REG_ICASE : 0) | RE.REG_MULTILINE, RESyntax.RE_SYNTAX_PERL5);
780 if (regexp == null)
781 {
782 endCompoundEdit();
783 return false;
784 }
785
786 if (replaceStr == null)
787 replaceStr = "";
788
789 REMatch match;
790
791 Element map = getDocument().getDefaultRootElement();
792 int startLine = map.getElementIndex(start);
793 int endLine = map.getElementIndex(end);
794
795 for (int i = startLine; i <= endLine; i++)
796 {
797 Element lineElement = map.getElement(i);
798 int lineStart;
799 int lineEnd;
800
801 if (i == startLine)
802 lineStart = start;
803 else
804 lineStart = lineElement.getStartOffset();
805 if (i == endLine)
806 lineEnd = end;
807 else
808 lineEnd = lineElement.getEndOffset() - 1;
809
810 lineEnd -= lineStart;
811 String line = getText(lineStart, lineEnd);
812 String newLine = regexp.substituteAll(line, replaceStr);
813 if (line.equals(newLine)) continue;
814 getDocument().remove(lineStart, lineEnd);
815 getDocument().insertString(lineStart, newLine, null);
816
817 end += (newLine.length() - lineEnd);
818 found = true;
819 }
820 } catch(Exception e) {
821 found = false;
822 }
823 endCompoundEdit();
824 return found;
825 }
826
827 /**
828 * When an undoable event is fired, we add it to the undo/redo list.
829 */
830
831 public void undoableEditHappened(UndoableEditEvent e)
832 {
833 if (!getOperation())
834 {
835 if (compoundEdit == null)
836 undo.addEdit(e.getEdit());
837 else
838 compoundEdit.addEdit(e.getEdit());
839 }
840 }
841
842 /**
843 * When a modification is made in the text, we turn
844 * the 'to_be_saved' flag to true.
845 */
846
847 public void changedUpdate(DocumentEvent e)
848 {
849 if (!getOperation() && !isDirty()) setDirty();
850 }
851
852 /**
853 * When a modification is made in the text, we turn
854 * the 'to_be_saved' flag to true.
855 */
856
857 public void insertUpdate(DocumentEvent e)
858 {
859 if (!getOperation() && !isDirty()) setDirty();
860 }
861
862 /**
863 * When a modification is made in the text, we turn
864 * the 'to_be_saved' flag to true.
865 */
866
867 public void removeUpdate(DocumentEvent e)
868 {
869 if (!getOperation() && !isDirty()) setDirty();
870 }
871
872 /**
873 * Return a String representation of this object.
874 */
875
876 public String toString()
877 {
878 StringBuffer buf = new StringBuffer();
879 buf.append("BuTextArea: ");
880 buf.append("[filename: " + getCurrentFile() + ";");
881 buf.append(" filesize: " + getLength() + "] -");
882 buf.append("[anchor: " + getAnchor() + "] -");
883 buf.append(" [font-name: " + getFontName() + ";");
884 buf.append(" font-style: " + getFontStyle() + ";");
885 buf.append(" font-size: " + getFontSize() + "]");
886 return buf.toString();
887 }
888
889
890 public static void main(String argv[])
891 {
892 JFrame frame=new JFrame("Test BuTextArea");
893 JComponent content=(JComponent)frame.getContentPane();
894 BuTextArea ta1=new BuTextArea();
895 BuTextArea ta2=new BuTextArea();
896 ta2.setDocument(ta1.getDocument());
897
898 content.setLayout(new GridLayout(1,2));
899 content.add(ta1);
900 content.add(ta2);
901 frame.pack();
902 frame.show();
903 frame.setLocation(200,200);
904 }
905 }
906