1 2 import com.sun.star.accessibility.AccessibleTextType; 3 import com.sun.star.accessibility.TextSegment; 4 import com.sun.star.accessibility.XAccessibleContext; 5 import com.sun.star.accessibility.XAccessibleText; 6 import com.sun.star.accessibility.XAccessibleEditableText; 7 8 import com.sun.star.awt.Rectangle; 9 import com.sun.star.awt.Point; 10 import com.sun.star.uno.UnoRuntime; 11 import com.sun.star.lang.IndexOutOfBoundsException; 12 import com.sun.star.beans.PropertyValue; 13 14 import java.util.Vector; 15 import java.awt.Container; 16 import java.awt.FlowLayout; 17 import java.awt.BorderLayout; 18 import java.awt.Color; 19 import java.awt.Component; 20 import java.awt.Graphics; 21 import java.awt.event.ActionListener; 22 import java.awt.event.ActionEvent; 23 import javax.swing.JDialog; 24 import javax.swing.JButton; 25 import javax.swing.JPanel; 26 import javax.swing.JLabel; 27 import javax.swing.Icon; 28 import javax.swing.JTextArea; 29 import javax.swing.JOptionPane; 30 import javax.swing.JCheckBox; 31 import javax.swing.JColorChooser; 32 import javax.swing.BoxLayout; 33 import javax.swing.text.JTextComponent; 34 35 36 class AccessibleTextHandler extends NodeHandler 37 { 38 public NodeHandler createHandler (XAccessibleContext xContext) 39 { 40 XAccessibleText xText = (XAccessibleText) UnoRuntime.queryInterface ( 41 XAccessibleText.class, xContext); 42 if (xText != null) 43 return new AccessibleTextHandler (xText); 44 else 45 return null; 46 } 47 48 public AccessibleTextHandler () 49 { 50 } 51 52 public AccessibleTextHandler (XAccessibleText xText) 53 { 54 if (xText != null) 55 maChildList.setSize (8); 56 } 57 58 public AccessibleTreeNode createChild (AccessibleTreeNode aParent, int nIndex) 59 { 60 AccessibleTreeNode aChild = null; 61 XAccessibleText xText = null; 62 if (aParent instanceof AccTreeNode) 63 xText = ((AccTreeNode)aParent).getText(); 64 65 try 66 { 67 if( xText != null ) 68 { 69 switch( nIndex ) 70 { 71 case 0: 72 aChild = new StringNode (xText.getText(), aParent); 73 break; 74 case 1: 75 aChild = new StringNode ("# chars: " + xText.getCharacterCount(), aParent); 76 break; 77 case 2: 78 aChild = new StringNode (characters( xText ), aParent); 79 break; 80 case 3: 81 aChild = new StringNode ("selection: " 82 + "[" + xText.getSelectionStart() 83 + "," + xText.getSelectionEnd() 84 + "] \"" + xText.getSelectedText() + "\"", 85 aParent); 86 break; 87 case 4: 88 aChild = new StringNode ("getCaretPosition: " + xText.getCaretPosition(), aParent); 89 break; 90 case 5: 91 { 92 VectorNode aVec = new VectorNode("portions", aParent); 93 aChild = aVec; 94 aVec.addChild( 95 textAtIndexNode( xText, "Character", 96 AccessibleTextType.CHARACTER, 97 aParent ) ); 98 aVec.addChild( 99 textAtIndexNode( xText, "Word", 100 AccessibleTextType.WORD, 101 aParent ) ); 102 aVec.addChild( 103 textAtIndexNode( xText, "Sentence", 104 AccessibleTextType.SENTENCE, 105 aParent ) ); 106 aVec.addChild( 107 textAtIndexNode( xText, "Paragraph", 108 AccessibleTextType.PARAGRAPH, 109 aParent ) ); 110 aVec.addChild( 111 textAtIndexNode( xText, "Line", 112 AccessibleTextType.LINE, 113 aParent ) ); 114 aVec.addChild( 115 textAtIndexNode( xText, "Attribute", 116 AccessibleTextType.ATTRIBUTE_RUN, 117 aParent ) ); 118 aVec.addChild( 119 textAtIndexNode( xText, "Glyph", 120 AccessibleTextType.GLYPH, 121 aParent ) ); 122 } 123 break; 124 case 6: 125 aChild = new StringNode (bounds( xText ), aParent); 126 break; 127 case 7: 128 aChild = getAttributes( xText, aParent ); 129 break; 130 default: 131 aChild = new StringNode ("unknown child index " + nIndex, aParent); 132 } 133 } 134 } 135 catch (Exception e) 136 { 137 // Return empty child. 138 } 139 140 return aChild; 141 } 142 143 144 private String textAtIndexNodeString( 145 int nStart, int nEnd, 146 String sWord, String sBefore, String sBehind) 147 { 148 return "[" + nStart + "," + nEnd + "] " 149 + "\"" + sWord + "\" \t" 150 + "(" + sBefore + "," 151 + "" + sBehind + ")"; 152 } 153 154 /** Create a text node that lists all strings of a particular text type 155 */ 156 private AccessibleTreeNode textAtIndexNode( 157 XAccessibleText xText, 158 String sName, 159 short nTextType, 160 AccessibleTreeNode aParent) 161 { 162 VectorNode aNode = new VectorNode (sName, aParent); 163 164 // get word at all positions; 165 // for nicer display, compare current word to previous one and 166 // make a new node for every interval, not for every word 167 int nLength = xText.getCharacterCount(); 168 if( nLength > 0 ) 169 { 170 try 171 { 172 // sWord + nStart mark the current word 173 // make a node as soon as a new one is found; close the last 174 // one at the end 175 TextSegment sWord = xText.getTextAtIndex(0, nTextType); 176 TextSegment sBefore = xText.getTextBeforeIndex(0, nTextType); 177 TextSegment sBehind = xText.getTextBehindIndex(0, nTextType); 178 int nStart = 0; 179 for(int i = 1; i < nLength; i++) 180 { 181 TextSegment sTmp = xText.getTextAtIndex(i, nTextType); 182 TextSegment sTBef = xText.getTextBeforeIndex(i, nTextType); 183 TextSegment sTBeh = xText.getTextBehindIndex(i, nTextType); 184 if( ! ( sTmp.equals( sWord ) && sTBef.equals( sBefore ) && 185 sTBeh.equals( sBehind ) ) ) 186 { 187 aNode.addChild (new StringNode (textAtIndexNodeString( 188 nStart, i, 189 sWord.SegmentText, sBefore.SegmentText, sBehind.SegmentText), aNode)); 190 sWord = sTmp; 191 sBefore = sTBef; 192 sBehind = sTBeh; 193 nStart = i; 194 } 195 196 // don't generate more than 50 children. 197 if (aNode.getChildCount() > 50) 198 { 199 sWord.SegmentText = "..."; 200 break; 201 } 202 } 203 aNode.addChild (new StringNode (textAtIndexNodeString( 204 nStart, nLength, 205 sWord.SegmentText, sBefore.SegmentText, sBehind.SegmentText), aNode)); 206 } 207 catch( IndexOutOfBoundsException e ) 208 { 209 aNode.addChild (new StringNode (e.toString(), aNode)); 210 } 211 catch (com.sun.star.lang.IllegalArgumentException e) 212 { 213 aNode.addChild (new StringNode (e.toString(), aNode)); 214 } 215 } 216 217 return aNode; 218 } 219 220 221 222 /** getCharacter (display as array string) */ 223 private String characters(XAccessibleText xText) 224 { 225 // get count (max. 30) 226 int nChars = xText.getCharacterCount(); 227 if( nChars > 30 ) 228 nChars = 30; 229 230 // build up string 231 StringBuffer aChars = new StringBuffer(); 232 try 233 { 234 aChars.append( "[" ); 235 for( int i = 0; i < nChars; i++) 236 { 237 aChars.append( xText.getCharacter(i) ); 238 aChars.append( "," ); 239 } 240 if( nChars > 0) 241 { 242 if( nChars == xText.getCharacterCount() ) 243 aChars.deleteCharAt( aChars.length() - 1 ); 244 else 245 aChars.append( "..." ); 246 } 247 aChars.append( "]" ); 248 } 249 catch( IndexOutOfBoundsException e ) 250 { 251 aChars.append( " ERROR " ); 252 } 253 254 // return result 255 return "getCharacters: " + aChars; 256 } 257 258 259 /** iterate over characters, and translate their positions 260 * back and forth */ 261 private String bounds( XAccessibleText xText ) 262 { 263 StringBuffer aBuffer = new StringBuffer( "bounds: " ); 264 try 265 { 266 // iterate over characters 267 int nCount = xText.getCharacterCount(); 268 for(int i = 0; i < nCount; i++ ) 269 { 270 // get bounds for this character 271 Rectangle aRect = xText.getCharacterBounds( i ); 272 273 // get the character by 'clicking' into the middle of 274 // the bounds 275 Point aMiddle = new Point(); 276 aMiddle.X = aRect.X + (aRect.Width / 2) - 1; 277 aMiddle.Y = aRect.Y + (aRect.Height / 2 ) - 1; 278 int nIndex = xText.getIndexAtPoint( aMiddle ); 279 280 // get the character, or a '#' for an illegal index 281 if( (nIndex >= 0) && (nIndex < xText.getCharacter(i)) ) 282 aBuffer.append( xText.getCharacter(nIndex) ); 283 else 284 aBuffer.append( '#' ); 285 } 286 } 287 catch( IndexOutOfBoundsException e ) 288 { ; } // ignore errors 289 290 return aBuffer.toString(); 291 } 292 293 294 private AccessibleTreeNode getAttributes( XAccessibleText xText, 295 AccessibleTreeNode aParent) 296 { 297 String[] aAttributeList = new String[] { 298 "CharBackColor", 299 "CharColor", 300 "CharEscapement", 301 "CharHeight", 302 "CharPosture", 303 "CharStrikeout", 304 "CharUnderline", 305 "CharWeight", 306 "ParaAdjust", 307 "ParaBottomMargin", 308 "ParaFirstLineIndent", 309 "ParaLeftMargin", 310 "ParaLineSpacing", 311 "ParaRightMargin", 312 "ParaTabStops"}; 313 314 AccessibleTreeNode aRet; 315 316 try 317 { 318 VectorNode aPortions = new VectorNode ("getAttributes", aParent); 319 320 int nIndex = 0; 321 int nLength = xText.getCharacterCount(); 322 while( nIndex < nLength ) 323 { 324 // get attribute run 325 String aPortion = null; 326 try 327 { 328 aPortion = xText.getTextAtIndex( 329 nIndex, AccessibleTextType.ATTRIBUTE_RUN).SegmentText; 330 } 331 catch(com.sun.star.lang.IllegalArgumentException e) 332 { 333 aPortion = new String (""); 334 } 335 336 // get attributes and make node with attribute children 337 PropertyValue[] aValues = xText.getCharacterAttributes(nIndex, aAttributeList); 338 VectorNode aAttrs = new VectorNode (aPortion, aPortions); 339 for( int i = 0; i < aValues.length; i++ ) 340 { 341 new StringNode( aValues[i].Name + ": " + aValues[i].Value, 342 aAttrs ); 343 } 344 345 // get next portion, but advance at least one 346 nIndex += (aPortion.length() > 0) ? aPortion.length() : 1; 347 } 348 349 aRet = aPortions; 350 } 351 catch( IndexOutOfBoundsException e ) 352 { 353 aRet = new StringNode( "Exception caught:" + e, aParent ); 354 } 355 356 return aRet; 357 } 358 359 360 static String[] aTextActions = 361 new String[] { "select...", "copy..." }; 362 static String[] aEditableTextActions = 363 new String[] { "select...", "copy...", 364 "cut...", "paste...", "edit...", "format..." }; 365 366 public String[] getActions (AccessibleTreeNode aNode) 367 { 368 XAccessibleEditableText xEText = null; 369 if (aNode instanceof AccTreeNode) 370 xEText = ((AccTreeNode)aNode).getEditText (); 371 372 return (xEText == null) ? aTextActions : aEditableTextActions; 373 } 374 375 public void performAction (AccessibleTreeNode aNode, int nIndex) 376 { 377 if ( ! (aNode instanceof AccTreeNode)) 378 return; 379 380 AccTreeNode aATNode = (AccTreeNode)aNode; 381 TextActionDialog aDialog = null; 382 383 // create proper dialog 384 switch( nIndex ) 385 { 386 case 0: 387 aDialog = new TextActionDialog( aATNode, 388 "Select range:", 389 "select" ) 390 { 391 boolean action( 392 JTextComponent aText, AccTreeNode aNode ) 393 throws IndexOutOfBoundsException 394 { 395 return aNode.getText().setSelection( 396 getSelectionStart(), 397 getSelectionEnd() ); 398 } 399 }; 400 break; 401 case 1: 402 aDialog = new TextActionDialog( aATNode, 403 "Select range and copy:", 404 "copy" ) 405 { 406 boolean action( 407 JTextComponent aText, AccTreeNode aNode ) 408 throws IndexOutOfBoundsException 409 { 410 return aNode.getText().copyText( 411 getSelectionStart(), 412 getSelectionEnd() ); 413 } 414 }; 415 break; 416 case 2: 417 aDialog = new TextActionDialog( aATNode, 418 "Select range and cut:", 419 "cut" ) 420 { 421 boolean action( 422 JTextComponent aText, AccTreeNode aNode ) 423 throws IndexOutOfBoundsException 424 { 425 return aNode.getEditText().cutText( 426 getSelectionStart(), 427 getSelectionEnd() ); 428 } 429 }; 430 break; 431 case 3: 432 aDialog = new TextActionDialog( aATNode, 433 "Place Caret and paste:", 434 "paste" ) 435 { 436 boolean action( 437 JTextComponent aText, AccTreeNode aNode ) 438 throws IndexOutOfBoundsException 439 { 440 return aNode.getEditText().pasteText( 441 aText.getCaretPosition() ); 442 } 443 }; 444 break; 445 case 4: 446 aDialog = new TextEditDialog( aATNode, "Edit text:", 447 "edit" ); 448 break; 449 case 5: 450 aDialog = new TextAttributeDialog( aATNode ); 451 break; 452 } 453 454 if( aDialog != null ) 455 aDialog.show(); 456 } 457 458 } 459 460 /** 461 * Display a dialog with a text field and a pair of cancel/do-it buttons 462 */ 463 class TextActionDialog extends JDialog 464 implements ActionListener 465 { 466 AccTreeNode aNode; 467 JTextArea aText; 468 String sName; 469 JCheckBox aIndexToggle; 470 471 public TextActionDialog( AccTreeNode aNd, 472 String sExplanation, 473 String sButtonText ) 474 { 475 super( AccessibilityWorkBench.Instance() ); 476 477 aNode = aNd; 478 sName = sButtonText; 479 init( sExplanation, aNode.getText().getText(), sButtonText ); 480 // setSize( getPreferredSize() ); 481 setSize( 350, 225 ); 482 } 483 484 /** build dialog */ 485 protected void init( String sExplanation, 486 String sText, 487 String sButtonText ) 488 { 489 setTitle( sName ); 490 491 // vertical stacking of the elements 492 Container aContent = getContentPane(); 493 // aContent.setLayout( new BorderLayout() ); 494 495 // label with explanation 496 if( sExplanation.length() > 0 ) 497 aContent.add( new JLabel( sExplanation ), BorderLayout.NORTH ); 498 499 // the text field 500 aText = new JTextArea(); 501 aText.setText( sText ); 502 aText.setColumns( Math.min( Math.max( 40, sText.length() ), 20 ) ); 503 aText.setRows( sText.length() / 40 + 1 ); 504 aText.setLineWrap( true ); 505 aText.setEditable( false ); 506 aContent.add( aText, BorderLayout.CENTER ); 507 508 JPanel aButtons = new JPanel(); 509 aButtons.setLayout( new FlowLayout() ); 510 aIndexToggle = new JCheckBox( "reverse selection" ); 511 aButtons.add( aIndexToggle ); 512 JButton aActionButton = new JButton( sButtonText ); 513 aActionButton.setActionCommand( "Action" ); 514 aActionButton.addActionListener( this ); 515 aButtons.add( aActionButton ); 516 JButton aCancelButton = new JButton( "cancel" ); 517 aCancelButton.setActionCommand( "Cancel" ); 518 aCancelButton.addActionListener( this ); 519 aButtons.add( aCancelButton ); 520 521 // add Panel with buttons 522 aContent.add( aButtons, BorderLayout.SOUTH ); 523 } 524 525 void cancel() 526 { 527 hide(); 528 dispose(); 529 } 530 531 void action() 532 { 533 String sError = null; 534 try 535 { 536 boolean bSuccess = action( aText, aNode ); 537 if( !bSuccess ) 538 sError = "Can't execute"; 539 } 540 catch( IndexOutOfBoundsException e ) 541 { 542 sError = "Index out of bounds"; 543 } 544 545 if( sError != null ) 546 JOptionPane.showMessageDialog( AccessibilityWorkBench.Instance(), 547 sError, sName, 548 JOptionPane.ERROR_MESSAGE); 549 550 cancel(); 551 } 552 553 public void actionPerformed(ActionEvent e) 554 { 555 String sCommand = e.getActionCommand(); 556 557 if( "Cancel".equals( sCommand ) ) 558 cancel(); 559 else if( "Action".equals( sCommand ) ) 560 action(); 561 } 562 563 564 int getSelectionStart() { return getSelection(true); } 565 int getSelectionEnd() { return getSelection(false); } 566 int getSelection(boolean bStart) 567 { 568 return ( bStart ^ aIndexToggle.isSelected() ) 569 ? aText.getSelectionStart() : aText.getSelectionEnd(); 570 } 571 572 573 574 /** override this for dialog-specific action */ 575 boolean action( JTextComponent aText, AccTreeNode aNode ) 576 throws IndexOutOfBoundsException 577 { 578 return false; 579 } 580 } 581 582 583 class TextEditDialog extends TextActionDialog 584 { 585 public TextEditDialog( AccTreeNode aNode, 586 String sExplanation, 587 String sButtonText ) 588 { 589 super( aNode, sExplanation, sButtonText ); 590 } 591 592 protected void init( String sExplanation, 593 String sText, 594 String sButtonText ) 595 { 596 super.init( sExplanation, sText, sButtonText ); 597 aText.setEditable( true ); 598 } 599 600 601 /** edit the text */ 602 boolean action( JTextComponent aText, AccTreeNode aNode ) 603 { 604 // is this text editable? if not, fudge you and return 605 XAccessibleEditableText xEdit = aNode.getEditText(); 606 return ( xEdit == null ) ? false : 607 updateText( xEdit, aText.getText() ); 608 } 609 610 611 /** update the text */ 612 boolean updateText( XAccessibleEditableText xEdit, String sNew ) 613 { 614 String sOld = xEdit.getText(); 615 616 // false alarm? Early out if no change was done! 617 if( sOld.equals( sNew ) ) 618 return false; 619 620 // get the minimum length of both strings 621 int nMinLength = sOld.length(); 622 if( sNew.length() < nMinLength ) 623 nMinLength = sNew.length(); 624 625 // count equal characters from front and end 626 int nFront = 0; 627 while( (nFront < nMinLength) && 628 (sNew.charAt(nFront) == sOld.charAt(nFront)) ) 629 nFront++; 630 int nBack = 0; 631 while( (nBack < nMinLength) && 632 ( sNew.charAt(sNew.length()-nBack-1) == 633 sOld.charAt(sOld.length()-nBack-1) ) ) 634 nBack++; 635 if( nFront + nBack > nMinLength ) 636 nBack = nMinLength - nFront; 637 638 // so... the first nFront and the last nBack characters 639 // are the same. Change the others! 640 String sDel = sOld.substring( nFront, sOld.length() - nBack ); 641 String sIns = sNew.substring( nFront, sNew.length() - nBack ); 642 643 System.out.println("edit text: " + 644 sOld.substring(0, nFront) + 645 " [ " + sDel + " -> " + sIns + " ] " + 646 sOld.substring(sOld.length() - nBack) ); 647 648 boolean bRet = false; 649 try 650 { 651 // edit the text, and use 652 // (set|insert|delete|replace)Text as needed 653 if( nFront+nBack == 0 ) 654 bRet = xEdit.setText( sIns ); 655 else if( sDel.length() == 0 ) 656 bRet = xEdit.insertText( sIns, nFront ); 657 else if( sIns.length() == 0 ) 658 bRet = xEdit.deleteText( nFront, sOld.length()-nBack ); 659 else 660 bRet = xEdit.replaceText(nFront, sOld.length()-nBack,sIns); 661 } 662 catch( IndexOutOfBoundsException e ) 663 { 664 bRet = false; 665 } 666 667 return bRet; 668 } 669 } 670 671 672 class TextAttributeDialog extends TextActionDialog 673 { 674 public TextAttributeDialog( 675 AccTreeNode aNode ) 676 { 677 super( aNode, "Choose attributes, select text, and press 'Set':", 678 "set" ); 679 } 680 681 private JCheckBox aBold, aUnderline, aItalics; 682 private Color aForeground, aBackground; 683 684 protected void init( String sExplanation, 685 String sText, 686 String sButtonText ) 687 { 688 super.init( sExplanation, sText, sButtonText ); 689 690 aForeground = Color.black; 691 aBackground = Color.white; 692 693 JPanel aAttr = new JPanel(); 694 aAttr.setLayout( new BoxLayout( aAttr, BoxLayout.Y_AXIS ) ); 695 696 aBold = new JCheckBox( "bold" ); 697 aUnderline = new JCheckBox( "underline" ); 698 aItalics = new JCheckBox( "italics" ); 699 700 JButton aForeButton = new JButton("Foreground", new ColorIcon(true)); 701 aForeButton.addActionListener( new ActionListener() { 702 public void actionPerformed(ActionEvent e) 703 { 704 aForeground = JColorChooser.showDialog( 705 TextAttributeDialog.this, 706 "Select Foreground Color", 707 aForeground); 708 } 709 } ); 710 711 JButton aBackButton = new JButton("Background", new ColorIcon(false)); 712 aBackButton.addActionListener( new ActionListener() { 713 public void actionPerformed(ActionEvent e) 714 { 715 aBackground = JColorChooser.showDialog( 716 TextAttributeDialog.this, 717 "Select Background Color", 718 aBackground); 719 } 720 } ); 721 722 aAttr.add( aBold ); 723 aAttr.add( aUnderline ); 724 aAttr.add( aItalics ); 725 aAttr.add( aForeButton ); 726 aAttr.add( aBackButton ); 727 728 getContentPane().add( aAttr, BorderLayout.WEST ); 729 } 730 731 732 class ColorIcon implements Icon 733 { 734 boolean bForeground; 735 static final int nHeight = 16; 736 static final int nWidth = 16; 737 738 public ColorIcon(boolean bWhich) { bForeground = bWhich; } 739 public int getIconHeight() { return nHeight; } 740 public int getIconWidth() { return nWidth; } 741 public void paintIcon(Component c, Graphics g, int x, int y) 742 { 743 g.setColor( getColor() ); 744 g.fillRect( x, y, nHeight, nWidth ); 745 g.setColor( c.getForeground() ); 746 g.drawRect( x, y, nHeight, nWidth ); 747 } 748 Color getColor() 749 { 750 return bForeground ? aForeground : aBackground; 751 } 752 } 753 754 755 756 /** edit the text */ 757 boolean action( JTextComponent aText, AccTreeNode aNode ) 758 throws IndexOutOfBoundsException 759 { 760 // is this text editable? if not, fudge you and return 761 XAccessibleEditableText xEdit = aNode.getEditText(); 762 boolean bSuccess = false; 763 if( xEdit != null ) 764 { 765 PropertyValue[] aSequence = new PropertyValue[6]; 766 aSequence[0] = new PropertyValue(); 767 aSequence[0].Name = "CharWeight"; 768 aSequence[0].Value = new Integer( aBold.isSelected() ? 150 : 100 ); 769 aSequence[1] = new PropertyValue(); 770 aSequence[1].Name = "CharUnderline"; 771 aSequence[1].Value = new Integer( aUnderline.isSelected() ? 1 : 0 ); 772 aSequence[2] = new PropertyValue(); 773 aSequence[2].Name = "CharBackColor"; 774 aSequence[2].Value = new Integer( aBackground.getRGB() ); 775 aSequence[3] = new PropertyValue(); 776 aSequence[3].Name = "CharColor"; 777 aSequence[3].Value = new Integer( aForeground.getRGB() ); 778 aSequence[4] = new PropertyValue(); 779 aSequence[4].Name = "CharPosture"; 780 aSequence[4].Value = new Integer( aItalics.isSelected() ? 1 : 0 ); 781 aSequence[5] = new PropertyValue(); 782 aSequence[5].Name = "CharBackTransparent"; 783 aSequence[5].Value = new Boolean( false ); 784 785 bSuccess = xEdit.setAttributes( getSelectionStart(), 786 getSelectionEnd(), 787 aSequence ); 788 } 789 return bSuccess; 790 } 791 792 } 793