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