bump product version to 5.0.4.1
[LibreOffice.git] / toolkit / test / accessibility / AccessibleTextHandler.java
blob714bcb5910218d14ba9e2c366aab23c6a4809416
1 /*
2 * This file is part of the LibreOffice project.
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 * This file incorporates work covered by the following license notice:
10 * Licensed to the Apache Software Foundation (ASF) under one or more
11 * contributor license agreements. See the NOTICE file distributed
12 * with this work for additional information regarding copyright
13 * ownership. The ASF licenses this file to you under the Apache
14 * License, Version 2.0 (the "License"); you may not use this file
15 * except in compliance with the License. You may obtain a copy of
16 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
19 import java.awt.BorderLayout;
20 import java.awt.Color;
21 import java.awt.Component;
22 import java.awt.Container;
23 import java.awt.FlowLayout;
24 import java.awt.Graphics;
25 import java.awt.event.ActionEvent;
26 import java.awt.event.ActionListener;
28 import javax.swing.BoxLayout;
29 import javax.swing.Icon;
30 import javax.swing.JButton;
31 import javax.swing.JCheckBox;
32 import javax.swing.JColorChooser;
33 import javax.swing.JDialog;
34 import javax.swing.JLabel;
35 import javax.swing.JOptionPane;
36 import javax.swing.JPanel;
37 import javax.swing.JTextArea;
38 import javax.swing.text.JTextComponent;
40 import com.sun.star.accessibility.AccessibleTextType;
41 import com.sun.star.accessibility.TextSegment;
42 import com.sun.star.accessibility.XAccessibleContext;
43 import com.sun.star.accessibility.XAccessibleEditableText;
44 import com.sun.star.accessibility.XAccessibleText;
45 import com.sun.star.awt.Point;
46 import com.sun.star.awt.Rectangle;
47 import com.sun.star.beans.PropertyValue;
48 import com.sun.star.beans.UnknownPropertyException;
49 import com.sun.star.lang.IndexOutOfBoundsException;
50 import com.sun.star.uno.UnoRuntime;
53 class AccessibleTextHandler extends NodeHandler
55 @Override
56 public NodeHandler createHandler (XAccessibleContext xContext)
58 XAccessibleText xText = UnoRuntime.queryInterface (
59 XAccessibleText.class, xContext);
60 if (xText != null)
61 return new AccessibleTextHandler (xText);
62 else
63 return null;
66 public AccessibleTextHandler ()
70 private AccessibleTextHandler (XAccessibleText xText)
72 if (xText != null)
73 maChildList.setSize (8);
76 @Override
77 public AccessibleTreeNode createChild (AccessibleTreeNode aParent, int nIndex)
79 AccessibleTreeNode aChild = null;
80 XAccessibleText xText = null;
81 if (aParent instanceof AccTreeNode)
82 xText = ((AccTreeNode)aParent).getText();
84 try
86 if( xText != null )
88 switch( nIndex )
90 case 0:
91 aChild = new StringNode (xText.getText(), aParent);
92 break;
93 case 1:
94 aChild = new StringNode ("# chars: " + xText.getCharacterCount(), aParent);
95 break;
96 case 2:
97 aChild = new StringNode (characters( xText ), aParent);
98 break;
99 case 3:
100 aChild = new StringNode ("selection: "
101 + "[" + xText.getSelectionStart()
102 + "," + xText.getSelectionEnd()
103 + "] \"" + xText.getSelectedText() + "\"",
104 aParent);
105 break;
106 case 4:
107 aChild = new StringNode ("getCaretPosition: " + xText.getCaretPosition(), aParent);
108 break;
109 case 5:
111 VectorNode aVec = new VectorNode("portions", aParent);
112 aChild = aVec;
113 aVec.addChild(
114 textAtIndexNode( xText, "Character",
115 AccessibleTextType.CHARACTER,
116 aParent ) );
117 aVec.addChild(
118 textAtIndexNode( xText, "Word",
119 AccessibleTextType.WORD,
120 aParent ) );
121 aVec.addChild(
122 textAtIndexNode( xText, "Sentence",
123 AccessibleTextType.SENTENCE,
124 aParent ) );
125 aVec.addChild(
126 textAtIndexNode( xText, "Paragraph",
127 AccessibleTextType.PARAGRAPH,
128 aParent ) );
129 aVec.addChild(
130 textAtIndexNode( xText, "Line",
131 AccessibleTextType.LINE,
132 aParent ) );
133 aVec.addChild(
134 textAtIndexNode( xText, "Attribute",
135 AccessibleTextType.ATTRIBUTE_RUN,
136 aParent ) );
137 aVec.addChild(
138 textAtIndexNode( xText, "Glyph",
139 AccessibleTextType.GLYPH,
140 aParent ) );
142 break;
143 case 6:
144 aChild = new StringNode (bounds( xText ), aParent);
145 break;
146 case 7:
147 aChild = getAttributes( xText, aParent );
148 break;
149 default:
150 aChild = new StringNode ("unknown child index " + nIndex, aParent);
154 catch (Exception e)
156 // Return empty child.
159 return aChild;
163 private String textAtIndexNodeString(
164 int nStart, int nEnd,
165 String sWord, String sBefore, String sBehind)
167 return "[" + nStart + "," + nEnd + "] "
168 + "\"" + sWord + "\" \t"
169 + "(" + sBefore + "," + sBehind + ")";
172 /** Create a text node that lists all strings of a particular text type
174 private AccessibleTreeNode textAtIndexNode(
175 XAccessibleText xText,
176 String sName,
177 short nTextType,
178 AccessibleTreeNode aParent)
180 VectorNode aNode = new VectorNode (sName, aParent);
182 // get word at all positions;
183 // for nicer display, compare current word to previous one and
184 // make a new node for every interval, not for every word
185 int nLength = xText.getCharacterCount();
186 if( nLength > 0 )
190 // sWord + nStart mark the current word
191 // make a node as soon as a new one is found; close the last
192 // one at the end
193 TextSegment sWord = xText.getTextAtIndex(0, nTextType);
194 TextSegment sBefore = xText.getTextBeforeIndex(0, nTextType);
195 TextSegment sBehind = xText.getTextBehindIndex(0, nTextType);
196 int nStart = 0;
197 for(int i = 1; i < nLength; i++)
199 TextSegment sTmp = xText.getTextAtIndex(i, nTextType);
200 TextSegment sTBef = xText.getTextBeforeIndex(i, nTextType);
201 TextSegment sTBeh = xText.getTextBehindIndex(i, nTextType);
202 if( ! ( sTmp.equals( sWord ) && sTBef.equals( sBefore ) &&
203 sTBeh.equals( sBehind ) ) )
205 aNode.addChild (new StringNode (textAtIndexNodeString(
206 nStart, i,
207 sWord.SegmentText, sBefore.SegmentText, sBehind.SegmentText), aNode));
208 sWord = sTmp;
209 sBefore = sTBef;
210 sBehind = sTBeh;
211 nStart = i;
214 // don't generate more than 50 children.
215 if (aNode.getChildCount() > 50)
217 sWord.SegmentText = "...";
218 break;
221 aNode.addChild (new StringNode (textAtIndexNodeString(
222 nStart, nLength,
223 sWord.SegmentText, sBefore.SegmentText, sBehind.SegmentText), aNode));
225 catch( IndexOutOfBoundsException e )
227 aNode.addChild (new StringNode (e.toString(), aNode));
229 catch (com.sun.star.lang.IllegalArgumentException e)
231 aNode.addChild (new StringNode (e.toString(), aNode));
235 return aNode;
240 /** getCharacter (display as array string) */
241 private String characters(XAccessibleText xText)
243 // get count (max. 30)
244 int nChars = xText.getCharacterCount();
245 if( nChars > 30 )
246 nChars = 30;
248 // build up string
249 StringBuffer aChars = new StringBuffer();
252 aChars.append( '[' );
253 for( int i = 0; i < nChars; i++)
255 aChars.append( xText.getCharacter(i) );
256 aChars.append( ',' );
258 if( nChars > 0)
260 if( nChars == xText.getCharacterCount() )
261 aChars.deleteCharAt( aChars.length() - 1 );
262 else
263 aChars.append( "..." );
265 aChars.append( ']' );
267 catch( IndexOutOfBoundsException e )
269 aChars.append( " ERROR " );
272 // return result
273 return "getCharacters: " + aChars;
277 /** iterate over characters, and translate their positions
278 * back and forth */
279 private String bounds( XAccessibleText xText )
281 StringBuffer aBuffer = new StringBuffer( "bounds: " );
284 // iterate over characters
285 int nCount = xText.getCharacterCount();
286 for(int i = 0; i < nCount; i++ )
288 // get bounds for this character
289 Rectangle aRect = xText.getCharacterBounds( i );
291 // get the character by 'clicking' into the middle of
292 // the bounds
293 Point aMiddle = new Point();
294 aMiddle.X = aRect.X + (aRect.Width / 2) - 1;
295 aMiddle.Y = aRect.Y + (aRect.Height / 2 ) - 1;
296 int nIndex = xText.getIndexAtPoint( aMiddle );
298 // get the character, or a '#' for an illegal index
299 if( (nIndex >= 0) && (nIndex < xText.getCharacter(i)) )
300 aBuffer.append( xText.getCharacter(nIndex) );
301 else
302 aBuffer.append( '#' );
305 catch( IndexOutOfBoundsException e )
306 { } // ignore errors
308 return aBuffer.toString();
312 private AccessibleTreeNode getAttributes( XAccessibleText xText,
313 AccessibleTreeNode aParent)
315 String[] aAttributeList = new String[] {
316 "CharBackColor",
317 "CharColor",
318 "CharEscapement",
319 "CharHeight",
320 "CharPosture",
321 "CharStrikeout",
322 "CharUnderline",
323 "CharWeight",
324 "ParaAdjust",
325 "ParaBottomMargin",
326 "ParaFirstLineIndent",
327 "ParaLeftMargin",
328 "ParaLineSpacing",
329 "ParaRightMargin",
330 "ParaTabStops"};
332 AccessibleTreeNode aRet;
336 VectorNode aPortions = new VectorNode ("getAttributes", aParent);
338 int nIndex = 0;
339 int nLength = xText.getCharacterCount();
340 while( nIndex < nLength )
342 // get attribute run
343 String aPortion = null;
346 aPortion = xText.getTextAtIndex(
347 nIndex, AccessibleTextType.ATTRIBUTE_RUN).SegmentText;
349 catch(com.sun.star.lang.IllegalArgumentException e)
351 aPortion = "";
354 // get attributes and make node with attribute children
355 PropertyValue[] aValues = xText.getCharacterAttributes(nIndex, aAttributeList);
356 VectorNode aAttrs = new VectorNode (aPortion, aPortions);
357 for( int i = 0; i < aValues.length; i++ )
359 new StringNode( aValues[i].Name + ": " + aValues[i].Value,
360 aAttrs );
363 // get next portion, but advance at least one
364 nIndex += (aPortion.length() > 0) ? aPortion.length() : 1;
367 aRet = aPortions;
369 catch( UnknownPropertyException e )
371 aRet = new StringNode( "Exception caught:" + e, aParent );
373 catch( IndexOutOfBoundsException e )
375 aRet = new StringNode( "Exception caught:" + e, aParent );
378 return aRet;
382 private static String[] aTextActions =
383 new String[] { "select...", "copy..." };
384 private static String[] aEditableTextActions =
385 new String[] { "select...", "copy...",
386 "cut...", "paste...", "edit...", "format..." };
388 @Override
389 public String[] getActions (AccessibleTreeNode aNode)
391 XAccessibleEditableText xEText = null;
392 if (aNode instanceof AccTreeNode)
393 xEText = ((AccTreeNode)aNode).getEditText ();
395 return (xEText == null) ? aTextActions : aEditableTextActions;
398 @Override
399 public void performAction (AccessibleTreeNode aNode, int nIndex)
401 if ( ! (aNode instanceof AccTreeNode))
402 return;
404 AccTreeNode aATNode = (AccTreeNode)aNode;
405 TextActionDialog aDialog = null;
407 // create proper dialog
408 switch( nIndex )
410 case 0:
411 aDialog = new TextActionDialog( aATNode,
412 "Select range:",
413 "select" )
415 @Override
416 boolean action(
417 JTextComponent aText, AccTreeNode aNode )
418 throws IndexOutOfBoundsException
420 return aNode.getText().setSelection(
421 getSelectionStart(),
422 getSelectionEnd() );
425 break;
426 case 1:
427 aDialog = new TextActionDialog( aATNode,
428 "Select range and copy:",
429 "copy" )
431 @Override
432 boolean action(
433 JTextComponent aText, AccTreeNode aNode )
434 throws IndexOutOfBoundsException
436 return aNode.getText().copyText(
437 getSelectionStart(),
438 getSelectionEnd() );
441 break;
442 case 2:
443 aDialog = new TextActionDialog( aATNode,
444 "Select range and cut:",
445 "cut" )
447 @Override
448 boolean action(
449 JTextComponent aText, AccTreeNode aNode )
450 throws IndexOutOfBoundsException
452 return aNode.getEditText().cutText(
453 getSelectionStart(),
454 getSelectionEnd() );
457 break;
458 case 3:
459 aDialog = new TextActionDialog( aATNode,
460 "Place Caret and paste:",
461 "paste" )
463 @Override
464 boolean action(
465 JTextComponent aText, AccTreeNode aNode )
466 throws IndexOutOfBoundsException
468 return aNode.getEditText().pasteText(
469 aText.getCaretPosition() );
472 break;
473 case 4:
474 aDialog = new TextEditDialog( aATNode, "Edit text:",
475 "edit" );
476 break;
477 case 5:
478 aDialog = new TextAttributeDialog( aATNode );
479 break;
482 if( aDialog != null )
483 aDialog.setVisible(true);
489 * Display a dialog with a text field and a pair of cancel/do-it buttons
491 abstract class TextActionDialog extends JDialog
492 implements ActionListener
494 private AccTreeNode aNode;
495 JTextArea aText;
496 private String sName;
497 private JCheckBox aIndexToggle;
499 public TextActionDialog( AccTreeNode aNd,
500 String sExplanation,
501 String sButtonText )
503 super( AccessibilityWorkBench.Instance() );
505 aNode = aNd;
506 sName = sButtonText;
507 init( sExplanation, aNode.getText().getText(), sButtonText );
508 setSize( 350, 225 );
511 /** build dialog */
512 protected void init( String sExplanation,
513 String sText,
514 String sButtonText )
516 setTitle( sName );
518 // vertical stacking of the elements
519 Container aContent = getContentPane();
521 // label with explanation
522 if( sExplanation.length() > 0 )
523 aContent.add( new JLabel( sExplanation ), BorderLayout.NORTH );
525 // the text field
526 aText = new JTextArea();
527 aText.setText( sText );
528 aText.setColumns( Math.min( Math.max( 40, sText.length() ), 20 ) );
529 aText.setRows( sText.length() / 40 + 1 );
530 aText.setLineWrap( true );
531 aText.setEditable( false );
532 aContent.add( aText, BorderLayout.CENTER );
534 JPanel aButtons = new JPanel();
535 aButtons.setLayout( new FlowLayout() );
536 aIndexToggle = new JCheckBox( "reverse selection" );
537 aButtons.add( aIndexToggle );
538 JButton aActionButton = new JButton( sButtonText );
539 aActionButton.setActionCommand( "Action" );
540 aActionButton.addActionListener( this );
541 aButtons.add( aActionButton );
542 JButton aCancelButton = new JButton( "cancel" );
543 aCancelButton.setActionCommand( "Cancel" );
544 aCancelButton.addActionListener( this );
545 aButtons.add( aCancelButton );
547 // add Panel with buttons
548 aContent.add( aButtons, BorderLayout.SOUTH );
551 private void cancel()
553 setVisible(false);
554 dispose();
557 private void action()
559 String sError = null;
562 boolean bSuccess = action( aText, aNode );
563 if( !bSuccess )
564 sError = "Can't execute";
566 catch( IndexOutOfBoundsException e )
568 sError = "Index out of bounds";
571 if( sError != null )
572 JOptionPane.showMessageDialog( AccessibilityWorkBench.Instance(),
573 sError, sName,
574 JOptionPane.ERROR_MESSAGE);
576 cancel();
579 public void actionPerformed(ActionEvent e)
581 String sCommand = e.getActionCommand();
583 if( "Cancel".equals( sCommand ) )
584 cancel();
585 else if( "Action".equals( sCommand ) )
586 action();
590 int getSelectionStart() { return getSelection(true); }
591 int getSelectionEnd() { return getSelection(false); }
592 private int getSelection(boolean bStart)
594 return ( bStart ^ aIndexToggle.isSelected() )
595 ? aText.getSelectionStart() : aText.getSelectionEnd();
600 /** override this for dialog-specific action */
601 abstract boolean action( JTextComponent aText, AccTreeNode aNode )
602 throws IndexOutOfBoundsException;
606 class TextEditDialog extends TextActionDialog
608 public TextEditDialog( AccTreeNode aNode,
609 String sExplanation,
610 String sButtonText )
612 super( aNode, sExplanation, sButtonText );
615 @Override
616 protected void init( String sExplanation,
617 String sText,
618 String sButtonText )
620 super.init( sExplanation, sText, sButtonText );
621 aText.setEditable( true );
625 /** edit the text */
626 @Override
627 boolean action( JTextComponent aText, AccTreeNode aNode )
629 // is this text editable? if not, fudge you and return
630 XAccessibleEditableText xEdit = aNode.getEditText();
631 return ( xEdit == null ) ? false :
632 updateText( xEdit, aText.getText() );
636 /** update the text */
637 private boolean updateText( XAccessibleEditableText xEdit, String sNew )
639 String sOld = xEdit.getText();
641 // false alarm? Early out if no change was done!
642 if( sOld.equals( sNew ) )
643 return false;
645 // get the minimum length of both strings
646 int nMinLength = sOld.length();
647 if( sNew.length() < nMinLength )
648 nMinLength = sNew.length();
650 // count equal characters from front and end
651 int nFront = 0;
652 while( (nFront < nMinLength) &&
653 (sNew.charAt(nFront) == sOld.charAt(nFront)) ) {
654 nFront++;
656 int nBack = 0;
657 while( (nBack < nMinLength) &&
658 ( sNew.charAt(sNew.length()-nBack-1) ==
659 sOld.charAt(sOld.length()-nBack-1) ) ) {
660 nBack++;
662 if( nFront + nBack > nMinLength )
663 nBack = nMinLength - nFront;
665 // so... the first nFront and the last nBack characters
666 // are the same. Change the others!
667 String sDel = sOld.substring( nFront, sOld.length() - nBack );
668 String sIns = sNew.substring( nFront, sNew.length() - nBack );
670 System.out.println("edit text: " +
671 sOld.substring(0, nFront) +
672 " [ " + sDel + " -> " + sIns + " ] " +
673 sOld.substring(sOld.length() - nBack) );
675 boolean bRet = false;
678 // edit the text, and use
679 // (set|insert|delete|replace)Text as needed
680 if( nFront+nBack == 0 )
681 bRet = xEdit.setText( sIns );
682 else if( sDel.length() == 0 )
683 bRet = xEdit.insertText( sIns, nFront );
684 else if( sIns.length() == 0 )
685 bRet = xEdit.deleteText( nFront, sOld.length()-nBack );
686 else
687 bRet = xEdit.replaceText(nFront, sOld.length()-nBack,sIns);
689 catch( IndexOutOfBoundsException e )
691 bRet = false;
694 return bRet;
699 class TextAttributeDialog extends TextActionDialog
701 public TextAttributeDialog(
702 AccTreeNode aNode )
704 super( aNode, "Choose attributes, select text, and press 'Set':",
705 "set" );
708 private JCheckBox aBold, aUnderline, aItalics;
709 private Color aForeground, aBackground;
711 @Override
712 protected void init( String sExplanation,
713 String sText,
714 String sButtonText )
716 super.init( sExplanation, sText, sButtonText );
718 aForeground = Color.black;
719 aBackground = Color.white;
721 JPanel aAttr = new JPanel();
722 aAttr.setLayout( new BoxLayout( aAttr, BoxLayout.Y_AXIS ) );
724 aBold = new JCheckBox( "bold" );
725 aUnderline = new JCheckBox( "underline" );
726 aItalics = new JCheckBox( "italics" );
728 JButton aForeButton = new JButton("Foreground", new ColorIcon(true));
729 aForeButton.addActionListener( new ActionListener() {
730 public void actionPerformed(ActionEvent e)
732 aForeground = JColorChooser.showDialog(
733 TextAttributeDialog.this,
734 "Select Foreground Color",
735 aForeground);
737 } );
739 JButton aBackButton = new JButton("Background", new ColorIcon(false));
740 aBackButton.addActionListener( new ActionListener() {
741 public void actionPerformed(ActionEvent e)
743 aBackground = JColorChooser.showDialog(
744 TextAttributeDialog.this,
745 "Select Background Color",
746 aBackground);
748 } );
750 aAttr.add( aBold );
751 aAttr.add( aUnderline );
752 aAttr.add( aItalics );
753 aAttr.add( aForeButton );
754 aAttr.add( aBackButton );
756 getContentPane().add( aAttr, BorderLayout.WEST );
760 private class ColorIcon implements Icon
762 private boolean bForeground;
763 private static final int nHeight = 16;
764 private static final int nWidth = 16;
766 public ColorIcon(boolean bWhich) { bForeground = bWhich; }
767 public int getIconHeight() { return nHeight; }
768 public int getIconWidth() { return nWidth; }
769 public void paintIcon(Component c, Graphics g, int x, int y)
771 g.setColor( getColor() );
772 g.fillRect( x, y, nHeight, nWidth );
773 g.setColor( c.getForeground() );
774 g.drawRect( x, y, nHeight, nWidth );
776 Color getColor()
778 return bForeground ? aForeground : aBackground;
784 /** edit the text */
785 @Override
786 boolean action( JTextComponent aText, AccTreeNode aNode )
787 throws IndexOutOfBoundsException
789 // is this text editable? if not, fudge you and return
790 XAccessibleEditableText xEdit = aNode.getEditText();
791 boolean bSuccess = false;
792 if( xEdit != null )
794 PropertyValue[] aSequence = new PropertyValue[6];
795 aSequence[0] = new PropertyValue();
796 aSequence[0].Name = "CharWeight";
797 aSequence[0].Value = Integer.valueOf( aBold.isSelected() ? 150 : 100 );
798 aSequence[1] = new PropertyValue();
799 aSequence[1].Name = "CharUnderline";
800 aSequence[1].Value = Integer.valueOf( aUnderline.isSelected() ? 1 : 0 );
801 aSequence[2] = new PropertyValue();
802 aSequence[2].Name = "CharBackColor";
803 aSequence[2].Value = Integer.valueOf( aBackground.getRGB() );
804 aSequence[3] = new PropertyValue();
805 aSequence[3].Name = "CharColor";
806 aSequence[3].Value = Integer.valueOf( aForeground.getRGB() );
807 aSequence[4] = new PropertyValue();
808 aSequence[4].Name = "CharPosture";
809 aSequence[4].Value = Integer.valueOf( aItalics.isSelected() ? 1 : 0 );
810 aSequence[5] = new PropertyValue();
811 aSequence[5].Name = "CharBackTransparent";
812 aSequence[5].Value = Boolean.FALSE;
814 bSuccess = xEdit.setAttributes( getSelectionStart(),
815 getSelectionEnd(),
816 aSequence );
818 return bSuccess;