J. Duke | 319a3b9 | 2007-12-01 00:00:00 +0000 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright 1997-2005 Sun Microsystems, Inc. All Rights Reserved. |
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | * |
| 5 | * This code is free software; you can redistribute it and/or modify it |
| 6 | * under the terms of the GNU General Public License version 2 only, as |
| 7 | * published by the Free Software Foundation. Sun designates this |
| 8 | * particular file as subject to the "Classpath" exception as provided |
| 9 | * by Sun in the LICENSE file that accompanied this code. |
| 10 | * |
| 11 | * This code is distributed in the hope that it will be useful, but WITHOUT |
| 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | * version 2 for more details (a copy is included in the LICENSE file that |
| 15 | * accompanied this code). |
| 16 | * |
| 17 | * You should have received a copy of the GNU General Public License version |
| 18 | * 2 along with this work; if not, write to the Free Software Foundation, |
| 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | * |
| 21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
| 22 | * CA 95054 USA or visit www.sun.com if you need additional information or |
| 23 | * have any questions. |
| 24 | */ |
| 25 | package javax.swing.text; |
| 26 | |
| 27 | import java.util.Vector; |
| 28 | import javax.swing.event.*; |
| 29 | |
| 30 | /** |
| 31 | * A plain document that maintains no character attributes. The |
| 32 | * default element structure for this document is a map of the lines in |
| 33 | * the text. The Element returned by getDefaultRootElement is |
| 34 | * a map of the lines, and each child element represents a line. |
| 35 | * This model does not maintain any character level attributes, |
| 36 | * but each line can be tagged with an arbitrary set of attributes. |
| 37 | * Line to offset, and offset to line translations can be quickly |
| 38 | * performed using the default root element. The structure information |
| 39 | * of the DocumentEvent's fired by edits will indicate the line |
| 40 | * structure changes. |
| 41 | * <p> |
| 42 | * The default content storage management is performed by a |
| 43 | * gapped buffer implementation (GapContent). It supports |
| 44 | * editing reasonably large documents with good efficiency when |
| 45 | * the edits are contiguous or clustered, as is typical. |
| 46 | * <p> |
| 47 | * <strong>Warning:</strong> |
| 48 | * Serialized objects of this class will not be compatible with |
| 49 | * future Swing releases. The current serialization support is |
| 50 | * appropriate for short term storage or RMI between applications running |
| 51 | * the same version of Swing. As of 1.4, support for long term storage |
| 52 | * of all JavaBeans<sup><font size="-2">TM</font></sup> |
| 53 | * has been added to the <code>java.beans</code> package. |
| 54 | * Please see {@link java.beans.XMLEncoder}. |
| 55 | * |
| 56 | * @author Timothy Prinzing |
| 57 | * @see Document |
| 58 | * @see AbstractDocument |
| 59 | */ |
| 60 | public class PlainDocument extends AbstractDocument { |
| 61 | |
| 62 | /** |
| 63 | * Name of the attribute that specifies the tab |
| 64 | * size for tabs contained in the content. The |
| 65 | * type for the value is Integer. |
| 66 | */ |
| 67 | public static final String tabSizeAttribute = "tabSize"; |
| 68 | |
| 69 | /** |
| 70 | * Name of the attribute that specifies the maximum |
| 71 | * length of a line, if there is a maximum length. |
| 72 | * The type for the value is Integer. |
| 73 | */ |
| 74 | public static final String lineLimitAttribute = "lineLimit"; |
| 75 | |
| 76 | /** |
| 77 | * Constructs a plain text document. A default model using |
| 78 | * <code>GapContent</code> is constructed and set. |
| 79 | */ |
| 80 | public PlainDocument() { |
| 81 | this(new GapContent()); |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Constructs a plain text document. A default root element is created, |
| 86 | * and the tab size set to 8. |
| 87 | * |
| 88 | * @param c the container for the content |
| 89 | */ |
| 90 | public PlainDocument(Content c) { |
| 91 | super(c); |
| 92 | putProperty(tabSizeAttribute, new Integer(8)); |
| 93 | defaultRoot = createDefaultRoot(); |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Inserts some content into the document. |
| 98 | * Inserting content causes a write lock to be held while the |
| 99 | * actual changes are taking place, followed by notification |
| 100 | * to the observers on the thread that grabbed the write lock. |
| 101 | * <p> |
| 102 | * This method is thread safe, although most Swing methods |
| 103 | * are not. Please see |
| 104 | * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How |
| 105 | * to Use Threads</A> for more information. |
| 106 | * |
| 107 | * @param offs the starting offset >= 0 |
| 108 | * @param str the string to insert; does nothing with null/empty strings |
| 109 | * @param a the attributes for the inserted content |
| 110 | * @exception BadLocationException the given insert position is not a valid |
| 111 | * position within the document |
| 112 | * @see Document#insertString |
| 113 | */ |
| 114 | public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { |
| 115 | // fields don't want to have multiple lines. We may provide a field-specific |
| 116 | // model in the future in which case the filtering logic here will no longer |
| 117 | // be needed. |
| 118 | Object filterNewlines = getProperty("filterNewlines"); |
| 119 | if ((filterNewlines instanceof Boolean) && filterNewlines.equals(Boolean.TRUE)) { |
| 120 | if ((str != null) && (str.indexOf('\n') >= 0)) { |
| 121 | StringBuffer filtered = new StringBuffer(str); |
| 122 | int n = filtered.length(); |
| 123 | for (int i = 0; i < n; i++) { |
| 124 | if (filtered.charAt(i) == '\n') { |
| 125 | filtered.setCharAt(i, ' '); |
| 126 | } |
| 127 | } |
| 128 | str = filtered.toString(); |
| 129 | } |
| 130 | } |
| 131 | super.insertString(offs, str, a); |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * Gets the default root element for the document model. |
| 136 | * |
| 137 | * @return the root |
| 138 | * @see Document#getDefaultRootElement |
| 139 | */ |
| 140 | public Element getDefaultRootElement() { |
| 141 | return defaultRoot; |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * Creates the root element to be used to represent the |
| 146 | * default document structure. |
| 147 | * |
| 148 | * @return the element base |
| 149 | */ |
| 150 | protected AbstractElement createDefaultRoot() { |
| 151 | BranchElement map = (BranchElement) createBranchElement(null, null); |
| 152 | Element line = createLeafElement(map, null, 0, 1); |
| 153 | Element[] lines = new Element[1]; |
| 154 | lines[0] = line; |
| 155 | map.replace(0, 0, lines); |
| 156 | return map; |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Get the paragraph element containing the given position. Since this |
| 161 | * document only models lines, it returns the line instead. |
| 162 | */ |
| 163 | public Element getParagraphElement(int pos){ |
| 164 | Element lineMap = getDefaultRootElement(); |
| 165 | return lineMap.getElement( lineMap.getElementIndex( pos ) ); |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Updates document structure as a result of text insertion. This |
| 170 | * will happen within a write lock. Since this document simply |
| 171 | * maps out lines, we refresh the line map. |
| 172 | * |
| 173 | * @param chng the change event describing the dit |
| 174 | * @param attr the set of attributes for the inserted text |
| 175 | */ |
| 176 | protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) { |
| 177 | removed.removeAllElements(); |
| 178 | added.removeAllElements(); |
| 179 | BranchElement lineMap = (BranchElement) getDefaultRootElement(); |
| 180 | int offset = chng.getOffset(); |
| 181 | int length = chng.getLength(); |
| 182 | if (offset > 0) { |
| 183 | offset -= 1; |
| 184 | length += 1; |
| 185 | } |
| 186 | int index = lineMap.getElementIndex(offset); |
| 187 | Element rmCandidate = lineMap.getElement(index); |
| 188 | int rmOffs0 = rmCandidate.getStartOffset(); |
| 189 | int rmOffs1 = rmCandidate.getEndOffset(); |
| 190 | int lastOffset = rmOffs0; |
| 191 | try { |
| 192 | if (s == null) { |
| 193 | s = new Segment(); |
| 194 | } |
| 195 | getContent().getChars(offset, length, s); |
| 196 | boolean hasBreaks = false; |
| 197 | for (int i = 0; i < length; i++) { |
| 198 | char c = s.array[s.offset + i]; |
| 199 | if (c == '\n') { |
| 200 | int breakOffset = offset + i + 1; |
| 201 | added.addElement(createLeafElement(lineMap, null, lastOffset, breakOffset)); |
| 202 | lastOffset = breakOffset; |
| 203 | hasBreaks = true; |
| 204 | } |
| 205 | } |
| 206 | if (hasBreaks) { |
| 207 | int rmCount = 1; |
| 208 | removed.addElement(rmCandidate); |
| 209 | if ((offset + length == rmOffs1) && (lastOffset != rmOffs1) && |
| 210 | ((index+1) < lineMap.getElementCount())) { |
| 211 | rmCount += 1; |
| 212 | Element e = lineMap.getElement(index+1); |
| 213 | removed.addElement(e); |
| 214 | rmOffs1 = e.getEndOffset(); |
| 215 | } |
| 216 | if (lastOffset < rmOffs1) { |
| 217 | added.addElement(createLeafElement(lineMap, null, lastOffset, rmOffs1)); |
| 218 | } |
| 219 | |
| 220 | Element[] aelems = new Element[added.size()]; |
| 221 | added.copyInto(aelems); |
| 222 | Element[] relems = new Element[removed.size()]; |
| 223 | removed.copyInto(relems); |
| 224 | ElementEdit ee = new ElementEdit(lineMap, index, relems, aelems); |
| 225 | chng.addEdit(ee); |
| 226 | lineMap.replace(index, relems.length, aelems); |
| 227 | } |
| 228 | if (Utilities.isComposedTextAttributeDefined(attr)) { |
| 229 | insertComposedTextUpdate(chng, attr); |
| 230 | } |
| 231 | } catch (BadLocationException e) { |
| 232 | throw new Error("Internal error: " + e.toString()); |
| 233 | } |
| 234 | super.insertUpdate(chng, attr); |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * Updates any document structure as a result of text removal. |
| 239 | * This will happen within a write lock. Since the structure |
| 240 | * represents a line map, this just checks to see if the |
| 241 | * removal spans lines. If it does, the two lines outside |
| 242 | * of the removal area are joined together. |
| 243 | * |
| 244 | * @param chng the change event describing the edit |
| 245 | */ |
| 246 | protected void removeUpdate(DefaultDocumentEvent chng) { |
| 247 | removed.removeAllElements(); |
| 248 | BranchElement map = (BranchElement) getDefaultRootElement(); |
| 249 | int offset = chng.getOffset(); |
| 250 | int length = chng.getLength(); |
| 251 | int line0 = map.getElementIndex(offset); |
| 252 | int line1 = map.getElementIndex(offset + length); |
| 253 | if (line0 != line1) { |
| 254 | // a line was removed |
| 255 | for (int i = line0; i <= line1; i++) { |
| 256 | removed.addElement(map.getElement(i)); |
| 257 | } |
| 258 | int p0 = map.getElement(line0).getStartOffset(); |
| 259 | int p1 = map.getElement(line1).getEndOffset(); |
| 260 | Element[] aelems = new Element[1]; |
| 261 | aelems[0] = createLeafElement(map, null, p0, p1); |
| 262 | Element[] relems = new Element[removed.size()]; |
| 263 | removed.copyInto(relems); |
| 264 | ElementEdit ee = new ElementEdit(map, line0, relems, aelems); |
| 265 | chng.addEdit(ee); |
| 266 | map.replace(line0, relems.length, aelems); |
| 267 | } else { |
| 268 | //Check for the composed text element |
| 269 | Element line = map.getElement(line0); |
| 270 | if (!line.isLeaf()) { |
| 271 | Element leaf = line.getElement(line.getElementIndex(offset)); |
| 272 | if (Utilities.isComposedTextElement(leaf)) { |
| 273 | Element[] aelem = new Element[1]; |
| 274 | aelem[0] = createLeafElement(map, null, |
| 275 | line.getStartOffset(), line.getEndOffset()); |
| 276 | Element[] relem = new Element[1]; |
| 277 | relem[0] = line; |
| 278 | ElementEdit ee = new ElementEdit(map, line0, relem, aelem); |
| 279 | chng.addEdit(ee); |
| 280 | map.replace(line0, 1, aelem); |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | super.removeUpdate(chng); |
| 285 | } |
| 286 | |
| 287 | // |
| 288 | // Inserts the composed text of an input method. The line element |
| 289 | // where the composed text is inserted into becomes an branch element |
| 290 | // which contains leaf elements of the composed text and the text |
| 291 | // backing store. |
| 292 | // |
| 293 | private void insertComposedTextUpdate(DefaultDocumentEvent chng, AttributeSet attr) { |
| 294 | added.removeAllElements(); |
| 295 | BranchElement lineMap = (BranchElement) getDefaultRootElement(); |
| 296 | int offset = chng.getOffset(); |
| 297 | int length = chng.getLength(); |
| 298 | int index = lineMap.getElementIndex(offset); |
| 299 | Element elem = lineMap.getElement(index); |
| 300 | int elemStart = elem.getStartOffset(); |
| 301 | int elemEnd = elem.getEndOffset(); |
| 302 | BranchElement[] abelem = new BranchElement[1]; |
| 303 | abelem[0] = (BranchElement) createBranchElement(lineMap, null); |
| 304 | Element[] relem = new Element[1]; |
| 305 | relem[0] = elem; |
| 306 | if (elemStart != offset) |
| 307 | added.addElement(createLeafElement(abelem[0], null, elemStart, offset)); |
| 308 | added.addElement(createLeafElement(abelem[0], attr, offset, offset+length)); |
| 309 | if (elemEnd != offset+length) |
| 310 | added.addElement(createLeafElement(abelem[0], null, offset+length, elemEnd)); |
| 311 | Element[] alelem = new Element[added.size()]; |
| 312 | added.copyInto(alelem); |
| 313 | ElementEdit ee = new ElementEdit(lineMap, index, relem, abelem); |
| 314 | chng.addEdit(ee); |
| 315 | |
| 316 | abelem[0].replace(0, 0, alelem); |
| 317 | lineMap.replace(index, 1, abelem); |
| 318 | } |
| 319 | |
| 320 | private AbstractElement defaultRoot; |
| 321 | private Vector added = new Vector(); // Vector<Element> |
| 322 | private Vector removed = new Vector(); // Vector<Element> |
| 323 | private transient Segment s; |
| 324 | } |