Showing the changes to java/pkg/javax/swing/text/DefaultFormatter.java (1 of 18352 files). Show all
Unified · Split
| @@ -0,0 +1,760 @@ | |||
| 1 | /* | ||
| 2 | * Copyright (c) 2000, 2010, Oracle and/or its affiliates. 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. Oracle designates this | ||
| 8 | * particular file as subject to the "Classpath" exception as provided | ||
| 9 | * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | ||
| 22 | * or visit www.oracle.com if you need additional information or have any | ||
| 23 | * questions. | ||
| 24 | */ | ||
| 25 | package javax.swing.text; | ||
| 26 | |||
| 27 | import java.io.Serializable; | ||
| 28 | import java.lang.reflect.*; | ||
| 29 | import java.text.ParseException; | ||
| 30 | import javax.swing.*; | ||
| 31 | import javax.swing.text.*; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * <code>DefaultFormatter</code> formats aribtrary objects. Formatting is done | ||
| 35 | * by invoking the <code>toString</code> method. In order to convert the | ||
| 36 | * value back to a String, your class must provide a constructor that | ||
| 37 | * takes a String argument. If no single argument constructor that takes a | ||
| 38 | * String is found, the returned value will be the String passed into | ||
| 39 | * <code>stringToValue</code>. | ||
| 40 | * <p> | ||
| 41 | * Instances of <code>DefaultFormatter</code> can not be used in multiple | ||
| 42 | * instances of <code>JFormattedTextField</code>. To obtain a copy of | ||
| 43 | * an already configured <code>DefaultFormatter</code>, use the | ||
| 44 | * <code>clone</code> method. | ||
| 45 | * <p> | ||
| 46 | * <strong>Warning:</strong> | ||
| 47 | * Serialized objects of this class will not be compatible with | ||
| 48 | * future Swing releases. The current serialization support is | ||
| 49 | * appropriate for short term storage or RMI between applications running | ||
| 50 | * the same version of Swing. As of 1.4, support for long term storage | ||
| 51 | * of all JavaBeans<sup><font size="-2">TM</font></sup> | ||
| 52 | * has been added to the <code>java.beans</code> package. | ||
| 53 | * Please see {@link java.beans.XMLEncoder}. | ||
| 54 | * | ||
| 55 | * @see javax.swing.JFormattedTextField.AbstractFormatter | ||
| 56 | * | ||
| 57 | * @since 1.4 | ||
| 58 | */ | ||
| 59 | public class DefaultFormatter extends JFormattedTextField.AbstractFormatter | ||
| 60 | implements Cloneable, Serializable { | ||
| 61 | /** Indicates if the value being edited must match the mask. */ | ||
| 62 | private boolean allowsInvalid; | ||
| 63 | |||
| 64 | /** If true, editing mode is in overwrite (or strikethough). */ | ||
| 65 | private boolean overwriteMode; | ||
| 66 | |||
| 67 | /** If true, any time a valid edit happens commitEdit is invoked. */ | ||
| 68 | private boolean commitOnEdit; | ||
| 69 | |||
| 70 | /** Class used to create new instances. */ | ||
| 71 | private Class<?> valueClass; | ||
| 72 | |||
| 73 | /** NavigationFilter that forwards calls back to DefaultFormatter. */ | ||
| 74 | private NavigationFilter navigationFilter; | ||
| 75 | |||
| 76 | /** DocumentFilter that forwards calls back to DefaultFormatter. */ | ||
| 77 | private DocumentFilter documentFilter; | ||
| 78 | |||
| 79 | /** Used during replace to track the region to replace. */ | ||
| 80 | transient ReplaceHolder replaceHolder; | ||
| 81 | |||
| 82 | |||
| 83 | /** | ||
| 84 | * Creates a DefaultFormatter. | ||
| 85 | */ | ||
| 86 | public DefaultFormatter() { | ||
| 87 | overwriteMode = true; | ||
| 88 | allowsInvalid = true; | ||
| 89 | } | ||
| 90 | |||
| 91 | /** | ||
| 92 | * Installs the <code>DefaultFormatter</code> onto a particular | ||
| 93 | * <code>JFormattedTextField</code>. | ||
| 94 | * This will invoke <code>valueToString</code> to convert the | ||
| 95 | * current value from the <code>JFormattedTextField</code> to | ||
| 96 | * a String. This will then install the <code>Action</code>s from | ||
| 97 | * <code>getActions</code>, the <code>DocumentFilter</code> | ||
| 98 | * returned from <code>getDocumentFilter</code> and the | ||
| 99 | * <code>NavigationFilter</code> returned from | ||
| 100 | * <code>getNavigationFilter</code> onto the | ||
| 101 | * <code>JFormattedTextField</code>. | ||
| 102 | * <p> | ||
| 103 | * Subclasses will typically only need to override this if they | ||
| 104 | * wish to install additional listeners on the | ||
| 105 | * <code>JFormattedTextField</code>. | ||
| 106 | * <p> | ||
| 107 | * If there is a <code>ParseException</code> in converting the | ||
| 108 | * current value to a String, this will set the text to an empty | ||
| 109 | * String, and mark the <code>JFormattedTextField</code> as being | ||
| 110 | * in an invalid state. | ||
| 111 | * <p> | ||
| 112 | * While this is a public method, this is typically only useful | ||
| 113 | * for subclassers of <code>JFormattedTextField</code>. | ||
| 114 | * <code>JFormattedTextField</code> will invoke this method at | ||
| 115 | * the appropriate times when the value changes, or its internal | ||
| 116 | * state changes. | ||
| 117 | * | ||
| 118 | * @param ftf JFormattedTextField to format for, may be null indicating | ||
| 119 | * uninstall from current JFormattedTextField. | ||
| 120 | */ | ||
| 121 | public void install(JFormattedTextField ftf) { | ||
| 122 | super.install(ftf); | ||
| 123 | positionCursorAtInitialLocation(); | ||
| 124 | } | ||
| 125 | |||
| 126 | /** | ||
| 127 | * Sets when edits are published back to the | ||
| 128 | * <code>JFormattedTextField</code>. If true, <code>commitEdit</code> | ||
| 129 | * is invoked after every valid edit (any time the text is edited). On | ||
| 130 | * the other hand, if this is false than the <code>DefaultFormatter</code> | ||
| 131 | * does not publish edits back to the <code>JFormattedTextField</code>. | ||
| 132 | * As such, the only time the value of the <code>JFormattedTextField</code> | ||
| 133 | * will change is when <code>commitEdit</code> is invoked on | ||
| 134 | * <code>JFormattedTextField</code>, typically when enter is pressed | ||
| 135 | * or focus leaves the <code>JFormattedTextField</code>. | ||
| 136 | * | ||
| 137 | * @param commit Used to indicate when edits are commited back to the | ||
| 138 | * JTextComponent | ||
| 139 | */ | ||
| 140 | public void setCommitsOnValidEdit(boolean commit) { | ||
| 141 | commitOnEdit = commit; | ||
| 142 | } | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Returns when edits are published back to the | ||
| 146 | * <code>JFormattedTextField</code>. | ||
| 147 | * | ||
| 148 | * @return true if edits are commited after evey valid edit | ||
| 149 | */ | ||
| 150 | public boolean getCommitsOnValidEdit() { | ||
| 151 | return commitOnEdit; | ||
| 152 | } | ||
| 153 | |||
| 154 | /** | ||
| 155 | * Configures the behavior when inserting characters. If | ||
| 156 | * <code>overwriteMode</code> is true (the default), new characters | ||
| 157 | * overwrite existing characters in the model. | ||
| 158 | * | ||
| 159 | * @param overwriteMode Indicates if overwrite or overstrike mode is used | ||
| 160 | */ | ||
| 161 | public void setOverwriteMode(boolean overwriteMode) { | ||
| 162 | this.overwriteMode = overwriteMode; | ||
| 163 | } | ||
| 164 | |||
| 165 | /** | ||
| 166 | * Returns the behavior when inserting characters. | ||
| 167 | * | ||
| 168 | * @return true if newly inserted characters overwrite existing characters | ||
| 169 | */ | ||
| 170 | public boolean getOverwriteMode() { | ||
| 171 | return overwriteMode; | ||
| 172 | } | ||
| 173 | |||
| 174 | /** | ||
| 175 | * Sets whether or not the value being edited is allowed to be invalid | ||
| 176 | * for a length of time (that is, <code>stringToValue</code> throws | ||
| 177 | * a <code>ParseException</code>). | ||
| 178 | * It is often convenient to allow the user to temporarily input an | ||
| 179 | * invalid value. | ||
| 180 | * | ||
| 181 | * @param allowsInvalid Used to indicate if the edited value must always | ||
| 182 | * be valid | ||
| 183 | */ | ||
| 184 | public void setAllowsInvalid(boolean allowsInvalid) { | ||
| 185 | this.allowsInvalid = allowsInvalid; | ||
| 186 | } | ||
| 187 | |||
| 188 | /** | ||
| 189 | * Returns whether or not the value being edited is allowed to be invalid | ||
| 190 | * for a length of time. | ||
| 191 | * | ||
| 192 | * @return false if the edited value must always be valid | ||
| 193 | */ | ||
| 194 | public boolean getAllowsInvalid() { | ||
| 195 | return allowsInvalid; | ||
| 196 | } | ||
| 197 | |||
| 198 | /** | ||
| 199 | * Sets that class that is used to create new Objects. If the | ||
| 200 | * passed in class does not have a single argument constructor that | ||
| 201 | * takes a String, String values will be used. | ||
| 202 | * | ||
| 203 | * @param valueClass Class used to construct return value from | ||
| 204 | * stringToValue | ||
| 205 | */ | ||
| 206 | public void setValueClass(Class<?> valueClass) { | ||
| 207 | this.valueClass = valueClass; | ||
| 208 | } | ||
| 209 | |||
| 210 | /** | ||
| 211 | * Returns that class that is used to create new Objects. | ||
| 212 | * | ||
| 213 | * @return Class used to constuct return value from stringToValue | ||
| 214 | */ | ||
| 215 | public Class<?> getValueClass() { | ||
| 216 | return valueClass; | ||
| 217 | } | ||
| 218 | |||
| 219 | /** | ||
| 220 | * Converts the passed in String into an instance of | ||
| 221 | * <code>getValueClass</code> by way of the constructor that | ||
| 222 | * takes a String argument. If <code>getValueClass</code> | ||
| 223 | * returns null, the Class of the current value in the | ||
| 224 | * <code>JFormattedTextField</code> will be used. If this is null, a | ||
| 225 | * String will be returned. If the constructor thows an exception, a | ||
| 226 | * <code>ParseException</code> will be thrown. If there is no single | ||
| 227 | * argument String constructor, <code>string</code> will be returned. | ||
| 228 | * | ||
| 229 | * @throws ParseException if there is an error in the conversion | ||
| 230 | * @param string String to convert | ||
| 231 | * @return Object representation of text | ||
| 232 | */ | ||
| 233 | public Object stringToValue(String string) throws ParseException { | ||
| 234 | Class<?> vc = getValueClass(); | ||
| 235 | JFormattedTextField ftf = getFormattedTextField(); | ||
| 236 | |||
| 237 | if (vc == null && ftf != null) { | ||
| 238 | Object value = ftf.getValue(); | ||
| 239 | |||
| 240 | if (value != null) { | ||
| 241 | vc = value.getClass(); | ||
| 242 | } | ||
| 243 | } | ||
| 244 | if (vc != null) { | ||
| 245 | Constructor cons; | ||
| 246 | |||
| 247 | try { | ||
| 248 | cons = vc.getConstructor(new Class[] { String.class }); | ||
| 249 | |||
| 250 | } catch (NoSuchMethodException nsme) { | ||
| 251 | cons = null; | ||
| 252 | } | ||
| 253 | |||
| 254 | if (cons != null) { | ||
| 255 | try { | ||
| 256 | return cons.newInstance(new Object[] { string }); | ||
| 257 | } catch (Throwable ex) { | ||
| 258 | throw new ParseException("Error creating instance", 0); | ||
| 259 | } | ||
| 260 | } | ||
| 261 | } | ||
| 262 | return string; | ||
| 263 | } | ||
| 264 | |||
| 265 | /** | ||
| 266 | * Converts the passed in Object into a String by way of the | ||
| 267 | * <code>toString</code> method. | ||
| 268 | * | ||
| 269 | * @throws ParseException if there is an error in the conversion | ||
| 270 | * @param value Value to convert | ||
| 271 | * @return String representation of value | ||
| 272 | */ | ||
| 273 | public String valueToString(Object value) throws ParseException { | ||
| 274 | if (value == null) { | ||
| 275 | return ""; | ||
| 276 | } | ||
| 277 | return value.toString(); | ||
| 278 | } | ||
| 279 | |||
| 280 | /** | ||
| 281 | * Returns the <code>DocumentFilter</code> used to restrict the characters | ||
| 282 | * that can be input into the <code>JFormattedTextField</code>. | ||
| 283 | * | ||
| 284 | * @return DocumentFilter to restrict edits | ||
| 285 | */ | ||
| 286 | protected DocumentFilter getDocumentFilter() { | ||
| 287 | if (documentFilter == null) { | ||
| 288 | documentFilter = new DefaultDocumentFilter(); | ||
| 289 | } | ||
| 290 | return documentFilter; | ||
| 291 | } | ||
| 292 | |||
| 293 | /** | ||
| 294 | * Returns the <code>NavigationFilter</code> used to restrict where the | ||
| 295 | * cursor can be placed. | ||
| 296 | * | ||
| 297 | * @return NavigationFilter to restrict navigation | ||
| 298 | */ | ||
| 299 | protected NavigationFilter getNavigationFilter() { | ||
| 300 | if (navigationFilter == null) { | ||
| 301 | navigationFilter = new DefaultNavigationFilter(); | ||
| 302 | } | ||
| 303 | return navigationFilter; | ||
| 304 | } | ||
| 305 | |||
| 306 | /** | ||
| 307 | * Creates a copy of the DefaultFormatter. | ||
| 308 | * | ||
| 309 | * @return copy of the DefaultFormatter | ||
| 310 | */ | ||
| 311 | public Object clone() throws CloneNotSupportedException { | ||
| 312 | DefaultFormatter formatter = (DefaultFormatter)super.clone(); | ||
| 313 | |||
| 314 | formatter.navigationFilter = null; | ||
| 315 | formatter.documentFilter = null; | ||
| 316 | formatter.replaceHolder = null; | ||
| 317 | return formatter; | ||
| 318 | } | ||
| 319 | |||
| 320 | |||
| 321 | /** | ||
| 322 | * Positions the cursor at the initial location. | ||
| 323 | */ | ||
| 324 | void positionCursorAtInitialLocation() { | ||
| 325 | JFormattedTextField ftf = getFormattedTextField(); | ||
| 326 | if (ftf != null) { | ||
| 327 | ftf.setCaretPosition(getInitialVisualPosition()); | ||
| 328 | } | ||
| 329 | } | ||
| 330 | |||
| 331 | /** | ||
| 332 | * Returns the initial location to position the cursor at. This forwards | ||
| 333 | * the call to <code>getNextNavigatableChar</code>. | ||
| 334 | */ | ||
| 335 | int getInitialVisualPosition() { | ||
| 336 | return getNextNavigatableChar(0, 1); | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * Subclasses should override this if they want cursor navigation | ||
| 341 | * to skip certain characters. A return value of false indicates | ||
| 342 | * the character at <code>offset</code> should be skipped when | ||
| 343 | * navigating throught the field. | ||
| 344 | */ | ||
| 345 | boolean isNavigatable(int offset) { | ||
| 346 | return true; | ||
| 347 | } | ||
| 348 | |||
| 349 | /** | ||
| 350 | * Returns true if the text in <code>text</code> can be inserted. This | ||
| 351 | * does not mean the text will ultimately be inserted, it is used if | ||
| 352 | * text can trivially reject certain characters. | ||
| 353 | */ | ||
| 354 | boolean isLegalInsertText(String text) { | ||
| 355 | return true; | ||
| 356 | } | ||
| 357 | |||
| 358 | /** | ||
| 359 | * Returns the next editable character starting at offset incrementing | ||
| 360 | * the offset by <code>direction</code>. | ||
| 361 | */ | ||
| 362 | private int getNextNavigatableChar(int offset, int direction) { | ||
| 363 | int max = getFormattedTextField().getDocument().getLength(); | ||
| 364 | |||
| 365 | while (offset >= 0 && offset < max) { | ||
| 366 | if (isNavigatable(offset)) { | ||
| 367 | return offset; | ||
| 368 | } | ||
| 369 | offset += direction; | ||
| 370 | } | ||
| 371 | return offset; | ||
| 372 | } | ||
| 373 | |||
| 374 | /** | ||
| 375 | * A convenience methods to return the result of deleting | ||
| 376 | * <code>deleteLength</code> characters at <code>offset</code> | ||
| 377 | * and inserting <code>replaceString</code> at <code>offset</code> | ||
| 378 | * in the current text field. | ||
| 379 | */ | ||
| 380 | String getReplaceString(int offset, int deleteLength, | ||
| 381 | String replaceString) { | ||
| 382 | String string = getFormattedTextField().getText(); | ||
| 383 | String result; | ||
| 384 | |||
| 385 | result = string.substring(0, offset); | ||
| 386 | if (replaceString != null) { | ||
| 387 | result += replaceString; | ||
| 388 | } | ||
| 389 | if (offset + deleteLength < string.length()) { | ||
| 390 | result += string.substring(offset + deleteLength); | ||
| 391 | } | ||
| 392 | return result; | ||
| 393 | } | ||
| 394 | |||
| 395 | /* | ||
| 396 | * Returns true if the operation described by <code>rh</code> will | ||
| 397 | * result in a legal edit. This may set the <code>value</code> | ||
| 398 | * field of <code>rh</code>. | ||
| 399 | */ | ||
| 400 | boolean isValidEdit(ReplaceHolder rh) { | ||
| 401 | if (!getAllowsInvalid()) { | ||
| 402 | String newString = getReplaceString(rh.offset, rh.length, rh.text); | ||
| 403 | |||
| 404 | try { | ||
| 405 | rh.value = stringToValue(newString); | ||
| 406 | |||
| 407 | return true; | ||
| 408 | } catch (ParseException pe) { | ||
| 409 | return false; | ||
| 410 | } | ||
| 411 | } | ||
| 412 | return true; | ||
| 413 | } | ||
| 414 | |||
| 415 | /** | ||
| 416 | * Invokes <code>commitEdit</code> on the JFormattedTextField. | ||
| 417 | */ | ||
| 418 | void commitEdit() throws ParseException { | ||
| 419 | JFormattedTextField ftf = getFormattedTextField(); | ||
| 420 | |||
| 421 | if (ftf != null) { | ||
| 422 | ftf.commitEdit(); | ||
| 423 | } | ||
| 424 | } | ||
| 425 | |||
| 426 | /** | ||
| 427 | * Pushes the value to the JFormattedTextField if the current value | ||
| 428 | * is valid and invokes <code>setEditValid</code> based on the | ||
| 429 | * validity of the value. | ||
| 430 | */ | ||
| 431 | void updateValue() { | ||
| 432 | updateValue(null); | ||
| 433 | } | ||
| 434 | |||
| 435 | /** | ||
| 436 | * Pushes the <code>value</code> to the editor if we are to | ||
| 437 | * commit on edits. If <code>value</code> is null, the current value | ||
| 438 | * will be obtained from the text component. | ||
| 439 | */ | ||
| 440 | void updateValue(Object value) { | ||
| 441 | try { | ||
| 442 | if (value == null) { | ||
| 443 | String string = getFormattedTextField().getText(); | ||
| 444 | |||
| 445 | value = stringToValue(string); | ||
| 446 | } | ||
| 447 | |||
| 448 | if (getCommitsOnValidEdit()) { | ||
| 449 | commitEdit(); | ||
| 450 | } | ||
| 451 | setEditValid(true); | ||
| 452 | } catch (ParseException pe) { | ||
| 453 | setEditValid(false); | ||
| 454 | } | ||
| 455 | } | ||
| 456 | |||
| 457 | /** | ||
| 458 | * Returns the next cursor position from offset by incrementing | ||
| 459 | * <code>direction</code>. This uses | ||
| 460 | * <code>getNextNavigatableChar</code> | ||
| 461 | * as well as constraining the location to the max position. | ||
| 462 | */ | ||
| 463 | int getNextCursorPosition(int offset, int direction) { | ||
| 464 | int newOffset = getNextNavigatableChar(offset, direction); | ||
| 465 | int max = getFormattedTextField().getDocument().getLength(); | ||
| 466 | |||
| 467 | if (!getAllowsInvalid()) { | ||
| 468 | if (direction == -1 && offset == newOffset) { | ||
| 469 | // Case where hit backspace and only characters before | ||
| 470 | // offset are fixed. | ||
| 471 | newOffset = getNextNavigatableChar(newOffset, 1); | ||
| 472 | if (newOffset >= max) { | ||
| 473 | newOffset = offset; | ||
| 474 | } | ||
| 475 | } | ||
| 476 | else if (direction == 1 && newOffset >= max) { | ||
| 477 | // Don't go beyond last editable character. | ||
| 478 | newOffset = getNextNavigatableChar(max - 1, -1); | ||
| 479 | if (newOffset < max) { | ||
| 480 | newOffset++; | ||
| 481 | } | ||
| 482 | } | ||
| 483 | } | ||
| 484 | return newOffset; | ||
| 485 | } | ||
| 486 | |||
| 487 | /** | ||
| 488 | * Resets the cursor by using getNextCursorPosition. | ||
| 489 | */ | ||
| 490 | void repositionCursor(int offset, int direction) { | ||
| 491 | getFormattedTextField().getCaret().setDot(getNextCursorPosition | ||
| 492 | (offset, direction)); | ||
| 493 | } | ||
| 494 | |||
| 495 | |||
| 496 | /** | ||
| 497 | * Finds the next navigatable character. | ||
| 498 | */ | ||
| 499 | int getNextVisualPositionFrom(JTextComponent text, int pos, | ||
| 500 | Position.Bias bias, int direction, | ||
| 501 | Position.Bias[] biasRet) | ||
| 502 | throws BadLocationException { | ||
| 503 | int value = text.getUI().getNextVisualPositionFrom(text, pos, bias, | ||
| 504 | direction, biasRet); | ||
| 505 | |||
| 506 | if (value == -1) { | ||
| 507 | return -1; | ||
| 508 | } | ||
| 509 | if (!getAllowsInvalid() && (direction == SwingConstants.EAST || | ||
| 510 | direction == SwingConstants.WEST)) { | ||
| 511 | int last = -1; | ||
| 512 | |||
| 513 | while (!isNavigatable(value) && value != last) { | ||
| 514 | last = value; | ||
| 515 | value = text.getUI().getNextVisualPositionFrom( | ||
| 516 | text, value, bias, direction,biasRet); | ||
| 517 | } | ||
| 518 | int max = getFormattedTextField().getDocument().getLength(); | ||
| 519 | if (last == value || value == max) { | ||
| 520 | if (value == 0) { | ||
| 521 | biasRet[0] = Position.Bias.Forward; | ||
| 522 | value = getInitialVisualPosition(); | ||
| 523 | } | ||
| 524 | if (value >= max && max > 0) { | ||
| 525 | // Pending: should not assume forward! | ||
| 526 | biasRet[0] = Position.Bias.Forward; | ||
| 527 | value = getNextNavigatableChar(max - 1, -1) + 1; | ||
| 528 | } | ||
| 529 | } | ||
| 530 | } | ||
| 531 | return value; | ||
| 532 | } | ||
| 533 | |||
| 534 | /** | ||
| 535 | * Returns true if the edit described by <code>rh</code> will result | ||
| 536 | * in a legal value. | ||
| 537 | */ | ||
| 538 | boolean canReplace(ReplaceHolder rh) { | ||
| 539 | return isValidEdit(rh); | ||
| 540 | } | ||
| 541 | |||
| 542 | /** | ||
| 543 | * DocumentFilter method, funnels into <code>replace</code>. | ||
| 544 | */ | ||
| 545 | void replace(DocumentFilter.FilterBypass fb, int offset, | ||
| 546 | int length, String text, | ||
| 547 | AttributeSet attrs) throws BadLocationException { | ||
| 548 | ReplaceHolder rh = getReplaceHolder(fb, offset, length, text, attrs); | ||
| 549 | |||
| 550 | replace(rh); | ||
| 551 | } | ||
| 552 | |||
| 553 | /** | ||
| 554 | * If the edit described by <code>rh</code> is legal, this will | ||
| 555 | * return true, commit the edit (if necessary) and update the cursor | ||
| 556 | * position. This forwards to <code>canReplace</code> and | ||
| 557 | * <code>isLegalInsertText</code> as necessary to determine if | ||
| 558 | * the edit is in fact legal. | ||
| 559 | * <p> | ||
| 560 | * All of the DocumentFilter methods funnel into here, you should | ||
| 561 | * generally only have to override this. | ||
| 562 | */ | ||
| 563 | boolean replace(ReplaceHolder rh) throws BadLocationException { | ||
| 564 | boolean valid = true; | ||
| 565 | int direction = 1; | ||
| 566 | |||
| 567 | if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) && | ||
| 568 | (getFormattedTextField().getSelectionStart() != rh.offset || | ||
| 569 | rh.length > 1)) { | ||
| 570 | direction = -1; | ||
| 571 | } | ||
| 572 | |||
| 573 | if (getOverwriteMode() && rh.text != null && | ||
| 574 | getFormattedTextField().getSelectedText() == null) | ||
| 575 | { | ||
| 576 | rh.length = Math.min(Math.max(rh.length, rh.text.length()), | ||
| 577 | rh.fb.getDocument().getLength() - rh.offset); | ||
| 578 | } | ||
| 579 | if ((rh.text != null && !isLegalInsertText(rh.text)) || | ||
| 580 | !canReplace(rh) || | ||
| 581 | (rh.length == 0 && (rh.text == null || rh.text.length() == 0))) { | ||
| 582 | valid = false; | ||
| 583 | } | ||
| 584 | if (valid) { | ||
| 585 | int cursor = rh.cursorPosition; | ||
| 586 | |||
| 587 | rh.fb.replace(rh.offset, rh.length, rh.text, rh.attrs); | ||
| 588 | if (cursor == -1) { | ||
| 589 | cursor = rh.offset; | ||
| 590 | if (direction == 1 && rh.text != null) { | ||
| 591 | cursor = rh.offset + rh.text.length(); | ||
| 592 | } | ||
| 593 | } | ||
| 594 | updateValue(rh.value); | ||
| 595 | repositionCursor(cursor, direction); | ||
| 596 | return true; | ||
| 597 | } | ||
| 598 | else { | ||
| 599 | invalidEdit(); | ||
| 600 | } | ||
| 601 | return false; | ||
| 602 | } | ||
| 603 | |||
| 604 | /** | ||
| 605 | * NavigationFilter method, subclasses that wish finer control should | ||
| 606 | * override this. | ||
| 607 | */ | ||
| 608 | void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias){ | ||
| 609 | fb.setDot(dot, bias); | ||
| 610 | } | ||
| 611 | |||
| 612 | /** | ||
| 613 | * NavigationFilter method, subclasses that wish finer control should | ||
| 614 | * override this. | ||
| 615 | */ | ||
| 616 | void moveDot(NavigationFilter.FilterBypass fb, int dot, | ||
| 617 | Position.Bias bias) { | ||
| 618 | fb.moveDot(dot, bias); | ||
| 619 | } | ||
| 620 | |||
| 621 | |||
| 622 | /** | ||
| 623 | * Returns the ReplaceHolder to track the replace of the specified | ||
| 624 | * text. | ||
| 625 | */ | ||
| 626 | ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset, | ||
| 627 | int length, String text, | ||
| 628 | AttributeSet attrs) { | ||
| 629 | if (replaceHolder == null) { | ||
| 630 | replaceHolder = new ReplaceHolder(); | ||
| 631 | } | ||
| 632 | replaceHolder.reset(fb, offset, length, text, attrs); | ||
| 633 | return replaceHolder; | ||
| 634 | } | ||
| 635 | |||
| 636 | |||
| 637 | /** | ||
| 638 | * ReplaceHolder is used to track where insert/remove/replace is | ||
| 639 | * going to happen. | ||
| 640 | */ | ||
| 641 | static class ReplaceHolder { | ||
| 642 | /** The FilterBypass that was passed to the DocumentFilter method. */ | ||
| 643 | DocumentFilter.FilterBypass fb; | ||
| 644 | /** Offset where the remove/insert is going to occur. */ | ||
| 645 | int offset; | ||
| 646 | /** Length of text to remove. */ | ||
| 647 | int length; | ||
| 648 | /** The text to insert, may be null. */ | ||
| 649 | String text; | ||
| 650 | /** AttributeSet to attach to text, may be null. */ | ||
| 651 | AttributeSet attrs; | ||
| 652 | /** The resulting value, this may never be set. */ | ||
| 653 | Object value; | ||
| 654 | /** Position the cursor should be adjusted from. If this is -1 | ||
| 655 | * the cursor position will be adjusted based on the direction of | ||
| 656 | * the replace (-1: offset, 1: offset + text.length()), otherwise | ||
| 657 | * the cursor position is adusted from this position. | ||
| 658 | */ | ||
| 659 | int cursorPosition; | ||
| 660 | |||
| 661 | void reset(DocumentFilter.FilterBypass fb, int offset, int length, | ||
| 662 | String text, AttributeSet attrs) { | ||
| 663 | this.fb = fb; | ||
| 664 | this.offset = offset; | ||
| 665 | this.length = length; | ||
| 666 | this.text = text; | ||
| 667 | this.attrs = attrs; | ||
| 668 | this.value = null; | ||
| 669 | cursorPosition = -1; | ||
| 670 | } | ||
| 671 | } | ||
| 672 | |||
| 673 | |||
| 674 | /** | ||
| 675 | * NavigationFilter implementation that calls back to methods with | ||
| 676 | * same name in DefaultFormatter. | ||
| 677 | */ | ||
| 678 | private class DefaultNavigationFilter extends NavigationFilter | ||
| 679 | implements Serializable { | ||
| 680 | public void setDot(FilterBypass fb, int dot, Position.Bias bias) { | ||
| 681 | JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); | ||
| 682 | if (tc.composedTextExists()) { | ||
| 683 | // bypass the filter | ||
| 684 | fb.setDot(dot, bias); | ||
| 685 | } else { | ||
| 686 | DefaultFormatter.this.setDot(fb, dot, bias); | ||
| 687 | } | ||
| 688 | } | ||
| 689 | |||
| 690 | public void moveDot(FilterBypass fb, int dot, Position.Bias bias) { | ||
| 691 | JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); | ||
| 692 | if (tc.composedTextExists()) { | ||
| 693 | // bypass the filter | ||
| 694 | fb.moveDot(dot, bias); | ||
| 695 | } else { | ||
| 696 | DefaultFormatter.this.moveDot(fb, dot, bias); | ||
| 697 | } | ||
| 698 | } | ||
| 699 | |||
| 700 | public int getNextVisualPositionFrom(JTextComponent text, int pos, | ||
| 701 | Position.Bias bias, | ||
| 702 | int direction, | ||
| 703 | Position.Bias[] biasRet) | ||
| 704 | throws BadLocationException { | ||
| 705 | if (text.composedTextExists()) { | ||
| 706 | // forward the call to the UI directly | ||
| 707 | return text.getUI().getNextVisualPositionFrom( | ||
| 708 | text, pos, bias, direction, biasRet); | ||
| 709 | } else { | ||
| 710 | return DefaultFormatter.this.getNextVisualPositionFrom( | ||
| 711 | text, pos, bias, direction, biasRet); | ||
| 712 | } | ||
| 713 | } | ||
| 714 | } | ||
| 715 | |||
| 716 | |||
| 717 | /** | ||
| 718 | * DocumentFilter implementation that calls back to the replace | ||
| 719 | * method of DefaultFormatter. | ||
| 720 | */ | ||
| 721 | private class DefaultDocumentFilter extends DocumentFilter implements | ||
| 722 | Serializable { | ||
| 723 | public void remove(FilterBypass fb, int offset, int length) throws | ||
| 724 | BadLocationException { | ||
| 725 | JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); | ||
| 726 | if (tc.composedTextExists()) { | ||
| 727 | // bypass the filter | ||
| 728 | fb.remove(offset, length); | ||
| 729 | } else { | ||
| 730 | DefaultFormatter.this.replace(fb, offset, length, null, null); | ||
| 731 | } | ||
| 732 | } | ||
| 733 | |||
| 734 | public void insertString(FilterBypass fb, int offset, | ||
| 735 | String string, AttributeSet attr) throws | ||
| 736 | BadLocationException { | ||
| 737 | JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); | ||
| 738 | if (tc.composedTextExists() || | ||
| 739 | Utilities.isComposedTextAttributeDefined(attr)) { | ||
| 740 | // bypass the filter | ||
| 741 | fb.insertString(offset, string, attr); | ||
| 742 | } else { | ||
| 743 | DefaultFormatter.this.replace(fb, offset, 0, string, attr); | ||
| 744 | } | ||
| 745 | } | ||
| 746 | |||
| 747 | public void replace(FilterBypass fb, int offset, int length, | ||
| 748 | String text, AttributeSet attr) throws | ||
| 749 | BadLocationException { | ||
| 750 | JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); | ||
| 751 | if (tc.composedTextExists() || | ||
| 752 | Utilities.isComposedTextAttributeDefined(attr)) { | ||
| 753 | // bypass the filter | ||
| 754 | fb.replace(offset, length, text, attr); | ||
| 755 | } else { | ||
| 756 | DefaultFormatter.this.replace(fb, offset, length, text, attr); | ||
| 757 | } | ||
| 758 | } | ||
| 759 | } | ||
| 760 | } | ||