diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
commit | 940b4d1848e8c70ab7642901a68594e8016caffc (patch) | |
tree | eb72f344ee6c3d9b80a7ecc079ea79e9fba8676d /toolkit/test/accessibility/AccessibleTextHandler.java | |
parent | Initial commit. (diff) | |
download | libreoffice-upstream.tar.xz libreoffice-upstream.zip |
Adding upstream version 1:7.0.4.upstream/1%7.0.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/test/accessibility/AccessibleTextHandler.java')
-rw-r--r-- | toolkit/test/accessibility/AccessibleTextHandler.java | 821 |
1 files changed, 821 insertions, 0 deletions
diff --git a/toolkit/test/accessibility/AccessibleTextHandler.java b/toolkit/test/accessibility/AccessibleTextHandler.java new file mode 100644 index 000000000..714bcb591 --- /dev/null +++ b/toolkit/test/accessibility/AccessibleTextHandler.java @@ -0,0 +1,821 @@ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BoxLayout; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.text.JTextComponent; + +import com.sun.star.accessibility.AccessibleTextType; +import com.sun.star.accessibility.TextSegment; +import com.sun.star.accessibility.XAccessibleContext; +import com.sun.star.accessibility.XAccessibleEditableText; +import com.sun.star.accessibility.XAccessibleText; +import com.sun.star.awt.Point; +import com.sun.star.awt.Rectangle; +import com.sun.star.beans.PropertyValue; +import com.sun.star.beans.UnknownPropertyException; +import com.sun.star.lang.IndexOutOfBoundsException; +import com.sun.star.uno.UnoRuntime; + + +class AccessibleTextHandler extends NodeHandler +{ + @Override + public NodeHandler createHandler (XAccessibleContext xContext) + { + XAccessibleText xText = UnoRuntime.queryInterface ( + XAccessibleText.class, xContext); + if (xText != null) + return new AccessibleTextHandler (xText); + else + return null; + } + + public AccessibleTextHandler () + { + } + + private AccessibleTextHandler (XAccessibleText xText) + { + if (xText != null) + maChildList.setSize (8); + } + + @Override + public AccessibleTreeNode createChild (AccessibleTreeNode aParent, int nIndex) + { + AccessibleTreeNode aChild = null; + XAccessibleText xText = null; + if (aParent instanceof AccTreeNode) + xText = ((AccTreeNode)aParent).getText(); + + try + { + if( xText != null ) + { + switch( nIndex ) + { + case 0: + aChild = new StringNode (xText.getText(), aParent); + break; + case 1: + aChild = new StringNode ("# chars: " + xText.getCharacterCount(), aParent); + break; + case 2: + aChild = new StringNode (characters( xText ), aParent); + break; + case 3: + aChild = new StringNode ("selection: " + + "[" + xText.getSelectionStart() + + "," + xText.getSelectionEnd() + + "] \"" + xText.getSelectedText() + "\"", + aParent); + break; + case 4: + aChild = new StringNode ("getCaretPosition: " + xText.getCaretPosition(), aParent); + break; + case 5: + { + VectorNode aVec = new VectorNode("portions", aParent); + aChild = aVec; + aVec.addChild( + textAtIndexNode( xText, "Character", + AccessibleTextType.CHARACTER, + aParent ) ); + aVec.addChild( + textAtIndexNode( xText, "Word", + AccessibleTextType.WORD, + aParent ) ); + aVec.addChild( + textAtIndexNode( xText, "Sentence", + AccessibleTextType.SENTENCE, + aParent ) ); + aVec.addChild( + textAtIndexNode( xText, "Paragraph", + AccessibleTextType.PARAGRAPH, + aParent ) ); + aVec.addChild( + textAtIndexNode( xText, "Line", + AccessibleTextType.LINE, + aParent ) ); + aVec.addChild( + textAtIndexNode( xText, "Attribute", + AccessibleTextType.ATTRIBUTE_RUN, + aParent ) ); + aVec.addChild( + textAtIndexNode( xText, "Glyph", + AccessibleTextType.GLYPH, + aParent ) ); + } + break; + case 6: + aChild = new StringNode (bounds( xText ), aParent); + break; + case 7: + aChild = getAttributes( xText, aParent ); + break; + default: + aChild = new StringNode ("unknown child index " + nIndex, aParent); + } + } + } + catch (Exception e) + { + // Return empty child. + } + + return aChild; + } + + + private String textAtIndexNodeString( + int nStart, int nEnd, + String sWord, String sBefore, String sBehind) + { + return "[" + nStart + "," + nEnd + "] " + + "\"" + sWord + "\" \t" + + "(" + sBefore + "," + sBehind + ")"; + } + + /** Create a text node that lists all strings of a particular text type + */ + private AccessibleTreeNode textAtIndexNode( + XAccessibleText xText, + String sName, + short nTextType, + AccessibleTreeNode aParent) + { + VectorNode aNode = new VectorNode (sName, aParent); + + // get word at all positions; + // for nicer display, compare current word to previous one and + // make a new node for every interval, not for every word + int nLength = xText.getCharacterCount(); + if( nLength > 0 ) + { + try + { + // sWord + nStart mark the current word + // make a node as soon as a new one is found; close the last + // one at the end + TextSegment sWord = xText.getTextAtIndex(0, nTextType); + TextSegment sBefore = xText.getTextBeforeIndex(0, nTextType); + TextSegment sBehind = xText.getTextBehindIndex(0, nTextType); + int nStart = 0; + for(int i = 1; i < nLength; i++) + { + TextSegment sTmp = xText.getTextAtIndex(i, nTextType); + TextSegment sTBef = xText.getTextBeforeIndex(i, nTextType); + TextSegment sTBeh = xText.getTextBehindIndex(i, nTextType); + if( ! ( sTmp.equals( sWord ) && sTBef.equals( sBefore ) && + sTBeh.equals( sBehind ) ) ) + { + aNode.addChild (new StringNode (textAtIndexNodeString( + nStart, i, + sWord.SegmentText, sBefore.SegmentText, sBehind.SegmentText), aNode)); + sWord = sTmp; + sBefore = sTBef; + sBehind = sTBeh; + nStart = i; + } + + // don't generate more than 50 children. + if (aNode.getChildCount() > 50) + { + sWord.SegmentText = "..."; + break; + } + } + aNode.addChild (new StringNode (textAtIndexNodeString( + nStart, nLength, + sWord.SegmentText, sBefore.SegmentText, sBehind.SegmentText), aNode)); + } + catch( IndexOutOfBoundsException e ) + { + aNode.addChild (new StringNode (e.toString(), aNode)); + } + catch (com.sun.star.lang.IllegalArgumentException e) + { + aNode.addChild (new StringNode (e.toString(), aNode)); + } + } + + return aNode; + } + + + + /** getCharacter (display as array string) */ + private String characters(XAccessibleText xText) + { + // get count (max. 30) + int nChars = xText.getCharacterCount(); + if( nChars > 30 ) + nChars = 30; + + // build up string + StringBuffer aChars = new StringBuffer(); + try + { + aChars.append( '[' ); + for( int i = 0; i < nChars; i++) + { + aChars.append( xText.getCharacter(i) ); + aChars.append( ',' ); + } + if( nChars > 0) + { + if( nChars == xText.getCharacterCount() ) + aChars.deleteCharAt( aChars.length() - 1 ); + else + aChars.append( "..." ); + } + aChars.append( ']' ); + } + catch( IndexOutOfBoundsException e ) + { + aChars.append( " ERROR " ); + } + + // return result + return "getCharacters: " + aChars; + } + + + /** iterate over characters, and translate their positions + * back and forth */ + private String bounds( XAccessibleText xText ) + { + StringBuffer aBuffer = new StringBuffer( "bounds: " ); + try + { + // iterate over characters + int nCount = xText.getCharacterCount(); + for(int i = 0; i < nCount; i++ ) + { + // get bounds for this character + Rectangle aRect = xText.getCharacterBounds( i ); + + // get the character by 'clicking' into the middle of + // the bounds + Point aMiddle = new Point(); + aMiddle.X = aRect.X + (aRect.Width / 2) - 1; + aMiddle.Y = aRect.Y + (aRect.Height / 2 ) - 1; + int nIndex = xText.getIndexAtPoint( aMiddle ); + + // get the character, or a '#' for an illegal index + if( (nIndex >= 0) && (nIndex < xText.getCharacter(i)) ) + aBuffer.append( xText.getCharacter(nIndex) ); + else + aBuffer.append( '#' ); + } + } + catch( IndexOutOfBoundsException e ) + { } // ignore errors + + return aBuffer.toString(); + } + + + private AccessibleTreeNode getAttributes( XAccessibleText xText, + AccessibleTreeNode aParent) + { + String[] aAttributeList = new String[] { + "CharBackColor", + "CharColor", + "CharEscapement", + "CharHeight", + "CharPosture", + "CharStrikeout", + "CharUnderline", + "CharWeight", + "ParaAdjust", + "ParaBottomMargin", + "ParaFirstLineIndent", + "ParaLeftMargin", + "ParaLineSpacing", + "ParaRightMargin", + "ParaTabStops"}; + + AccessibleTreeNode aRet; + + try + { + VectorNode aPortions = new VectorNode ("getAttributes", aParent); + + int nIndex = 0; + int nLength = xText.getCharacterCount(); + while( nIndex < nLength ) + { + // get attribute run + String aPortion = null; + try + { + aPortion = xText.getTextAtIndex( + nIndex, AccessibleTextType.ATTRIBUTE_RUN).SegmentText; + } + catch(com.sun.star.lang.IllegalArgumentException e) + { + aPortion = ""; + } + + // get attributes and make node with attribute children + PropertyValue[] aValues = xText.getCharacterAttributes(nIndex, aAttributeList); + VectorNode aAttrs = new VectorNode (aPortion, aPortions); + for( int i = 0; i < aValues.length; i++ ) + { + new StringNode( aValues[i].Name + ": " + aValues[i].Value, + aAttrs ); + } + + // get next portion, but advance at least one + nIndex += (aPortion.length() > 0) ? aPortion.length() : 1; + } + + aRet = aPortions; + } + catch( UnknownPropertyException e ) + { + aRet = new StringNode( "Exception caught:" + e, aParent ); + } + catch( IndexOutOfBoundsException e ) + { + aRet = new StringNode( "Exception caught:" + e, aParent ); + } + + return aRet; + } + + + private static String[] aTextActions = + new String[] { "select...", "copy..." }; + private static String[] aEditableTextActions = + new String[] { "select...", "copy...", + "cut...", "paste...", "edit...", "format..." }; + + @Override + public String[] getActions (AccessibleTreeNode aNode) + { + XAccessibleEditableText xEText = null; + if (aNode instanceof AccTreeNode) + xEText = ((AccTreeNode)aNode).getEditText (); + + return (xEText == null) ? aTextActions : aEditableTextActions; + } + + @Override + public void performAction (AccessibleTreeNode aNode, int nIndex) + { + if ( ! (aNode instanceof AccTreeNode)) + return; + + AccTreeNode aATNode = (AccTreeNode)aNode; + TextActionDialog aDialog = null; + + // create proper dialog + switch( nIndex ) + { + case 0: + aDialog = new TextActionDialog( aATNode, + "Select range:", + "select" ) + { + @Override + boolean action( + JTextComponent aText, AccTreeNode aNode ) + throws IndexOutOfBoundsException + { + return aNode.getText().setSelection( + getSelectionStart(), + getSelectionEnd() ); + } + }; + break; + case 1: + aDialog = new TextActionDialog( aATNode, + "Select range and copy:", + "copy" ) + { + @Override + boolean action( + JTextComponent aText, AccTreeNode aNode ) + throws IndexOutOfBoundsException + { + return aNode.getText().copyText( + getSelectionStart(), + getSelectionEnd() ); + } + }; + break; + case 2: + aDialog = new TextActionDialog( aATNode, + "Select range and cut:", + "cut" ) + { + @Override + boolean action( + JTextComponent aText, AccTreeNode aNode ) + throws IndexOutOfBoundsException + { + return aNode.getEditText().cutText( + getSelectionStart(), + getSelectionEnd() ); + } + }; + break; + case 3: + aDialog = new TextActionDialog( aATNode, + "Place Caret and paste:", + "paste" ) + { + @Override + boolean action( + JTextComponent aText, AccTreeNode aNode ) + throws IndexOutOfBoundsException + { + return aNode.getEditText().pasteText( + aText.getCaretPosition() ); + } + }; + break; + case 4: + aDialog = new TextEditDialog( aATNode, "Edit text:", + "edit" ); + break; + case 5: + aDialog = new TextAttributeDialog( aATNode ); + break; + } + + if( aDialog != null ) + aDialog.setVisible(true); + } + +} + +/** + * Display a dialog with a text field and a pair of cancel/do-it buttons + */ +abstract class TextActionDialog extends JDialog + implements ActionListener +{ + private AccTreeNode aNode; + JTextArea aText; + private String sName; + private JCheckBox aIndexToggle; + + public TextActionDialog( AccTreeNode aNd, + String sExplanation, + String sButtonText ) + { + super( AccessibilityWorkBench.Instance() ); + + aNode = aNd; + sName = sButtonText; + init( sExplanation, aNode.getText().getText(), sButtonText ); + setSize( 350, 225 ); + } + + /** build dialog */ + protected void init( String sExplanation, + String sText, + String sButtonText ) + { + setTitle( sName ); + + // vertical stacking of the elements + Container aContent = getContentPane(); + + // label with explanation + if( sExplanation.length() > 0 ) + aContent.add( new JLabel( sExplanation ), BorderLayout.NORTH ); + + // the text field + aText = new JTextArea(); + aText.setText( sText ); + aText.setColumns( Math.min( Math.max( 40, sText.length() ), 20 ) ); + aText.setRows( sText.length() / 40 + 1 ); + aText.setLineWrap( true ); + aText.setEditable( false ); + aContent.add( aText, BorderLayout.CENTER ); + + JPanel aButtons = new JPanel(); + aButtons.setLayout( new FlowLayout() ); + aIndexToggle = new JCheckBox( "reverse selection" ); + aButtons.add( aIndexToggle ); + JButton aActionButton = new JButton( sButtonText ); + aActionButton.setActionCommand( "Action" ); + aActionButton.addActionListener( this ); + aButtons.add( aActionButton ); + JButton aCancelButton = new JButton( "cancel" ); + aCancelButton.setActionCommand( "Cancel" ); + aCancelButton.addActionListener( this ); + aButtons.add( aCancelButton ); + + // add Panel with buttons + aContent.add( aButtons, BorderLayout.SOUTH ); + } + + private void cancel() + { + setVisible(false); + dispose(); + } + + private void action() + { + String sError = null; + try + { + boolean bSuccess = action( aText, aNode ); + if( !bSuccess ) + sError = "Can't execute"; + } + catch( IndexOutOfBoundsException e ) + { + sError = "Index out of bounds"; + } + + if( sError != null ) + JOptionPane.showMessageDialog( AccessibilityWorkBench.Instance(), + sError, sName, + JOptionPane.ERROR_MESSAGE); + + cancel(); + } + + public void actionPerformed(ActionEvent e) + { + String sCommand = e.getActionCommand(); + + if( "Cancel".equals( sCommand ) ) + cancel(); + else if( "Action".equals( sCommand ) ) + action(); + } + + + int getSelectionStart() { return getSelection(true); } + int getSelectionEnd() { return getSelection(false); } + private int getSelection(boolean bStart) + { + return ( bStart ^ aIndexToggle.isSelected() ) + ? aText.getSelectionStart() : aText.getSelectionEnd(); + } + + + + /** override this for dialog-specific action */ + abstract boolean action( JTextComponent aText, AccTreeNode aNode ) + throws IndexOutOfBoundsException; +} + + +class TextEditDialog extends TextActionDialog +{ + public TextEditDialog( AccTreeNode aNode, + String sExplanation, + String sButtonText ) + { + super( aNode, sExplanation, sButtonText ); + } + + @Override + protected void init( String sExplanation, + String sText, + String sButtonText ) + { + super.init( sExplanation, sText, sButtonText ); + aText.setEditable( true ); + } + + + /** edit the text */ + @Override + boolean action( JTextComponent aText, AccTreeNode aNode ) + { + // is this text editable? if not, fudge you and return + XAccessibleEditableText xEdit = aNode.getEditText(); + return ( xEdit == null ) ? false : + updateText( xEdit, aText.getText() ); + } + + + /** update the text */ + private boolean updateText( XAccessibleEditableText xEdit, String sNew ) + { + String sOld = xEdit.getText(); + + // false alarm? Early out if no change was done! + if( sOld.equals( sNew ) ) + return false; + + // get the minimum length of both strings + int nMinLength = sOld.length(); + if( sNew.length() < nMinLength ) + nMinLength = sNew.length(); + + // count equal characters from front and end + int nFront = 0; + while( (nFront < nMinLength) && + (sNew.charAt(nFront) == sOld.charAt(nFront)) ) { + nFront++; + } + int nBack = 0; + while( (nBack < nMinLength) && + ( sNew.charAt(sNew.length()-nBack-1) == + sOld.charAt(sOld.length()-nBack-1) ) ) { + nBack++; + } + if( nFront + nBack > nMinLength ) + nBack = nMinLength - nFront; + + // so... the first nFront and the last nBack characters + // are the same. Change the others! + String sDel = sOld.substring( nFront, sOld.length() - nBack ); + String sIns = sNew.substring( nFront, sNew.length() - nBack ); + + System.out.println("edit text: " + + sOld.substring(0, nFront) + + " [ " + sDel + " -> " + sIns + " ] " + + sOld.substring(sOld.length() - nBack) ); + + boolean bRet = false; + try + { + // edit the text, and use + // (set|insert|delete|replace)Text as needed + if( nFront+nBack == 0 ) + bRet = xEdit.setText( sIns ); + else if( sDel.length() == 0 ) + bRet = xEdit.insertText( sIns, nFront ); + else if( sIns.length() == 0 ) + bRet = xEdit.deleteText( nFront, sOld.length()-nBack ); + else + bRet = xEdit.replaceText(nFront, sOld.length()-nBack,sIns); + } + catch( IndexOutOfBoundsException e ) + { + bRet = false; + } + + return bRet; + } +} + + +class TextAttributeDialog extends TextActionDialog +{ + public TextAttributeDialog( + AccTreeNode aNode ) + { + super( aNode, "Choose attributes, select text, and press 'Set':", + "set" ); + } + + private JCheckBox aBold, aUnderline, aItalics; + private Color aForeground, aBackground; + + @Override + protected void init( String sExplanation, + String sText, + String sButtonText ) + { + super.init( sExplanation, sText, sButtonText ); + + aForeground = Color.black; + aBackground = Color.white; + + JPanel aAttr = new JPanel(); + aAttr.setLayout( new BoxLayout( aAttr, BoxLayout.Y_AXIS ) ); + + aBold = new JCheckBox( "bold" ); + aUnderline = new JCheckBox( "underline" ); + aItalics = new JCheckBox( "italics" ); + + JButton aForeButton = new JButton("Foreground", new ColorIcon(true)); + aForeButton.addActionListener( new ActionListener() { + public void actionPerformed(ActionEvent e) + { + aForeground = JColorChooser.showDialog( + TextAttributeDialog.this, + "Select Foreground Color", + aForeground); + } + } ); + + JButton aBackButton = new JButton("Background", new ColorIcon(false)); + aBackButton.addActionListener( new ActionListener() { + public void actionPerformed(ActionEvent e) + { + aBackground = JColorChooser.showDialog( + TextAttributeDialog.this, + "Select Background Color", + aBackground); + } + } ); + + aAttr.add( aBold ); + aAttr.add( aUnderline ); + aAttr.add( aItalics ); + aAttr.add( aForeButton ); + aAttr.add( aBackButton ); + + getContentPane().add( aAttr, BorderLayout.WEST ); + } + + + private class ColorIcon implements Icon + { + private boolean bForeground; + private static final int nHeight = 16; + private static final int nWidth = 16; + + public ColorIcon(boolean bWhich) { bForeground = bWhich; } + public int getIconHeight() { return nHeight; } + public int getIconWidth() { return nWidth; } + public void paintIcon(Component c, Graphics g, int x, int y) + { + g.setColor( getColor() ); + g.fillRect( x, y, nHeight, nWidth ); + g.setColor( c.getForeground() ); + g.drawRect( x, y, nHeight, nWidth ); + } + Color getColor() + { + return bForeground ? aForeground : aBackground; + } + } + + + + /** edit the text */ + @Override + boolean action( JTextComponent aText, AccTreeNode aNode ) + throws IndexOutOfBoundsException + { + // is this text editable? if not, fudge you and return + XAccessibleEditableText xEdit = aNode.getEditText(); + boolean bSuccess = false; + if( xEdit != null ) + { + PropertyValue[] aSequence = new PropertyValue[6]; + aSequence[0] = new PropertyValue(); + aSequence[0].Name = "CharWeight"; + aSequence[0].Value = Integer.valueOf( aBold.isSelected() ? 150 : 100 ); + aSequence[1] = new PropertyValue(); + aSequence[1].Name = "CharUnderline"; + aSequence[1].Value = Integer.valueOf( aUnderline.isSelected() ? 1 : 0 ); + aSequence[2] = new PropertyValue(); + aSequence[2].Name = "CharBackColor"; + aSequence[2].Value = Integer.valueOf( aBackground.getRGB() ); + aSequence[3] = new PropertyValue(); + aSequence[3].Name = "CharColor"; + aSequence[3].Value = Integer.valueOf( aForeground.getRGB() ); + aSequence[4] = new PropertyValue(); + aSequence[4].Name = "CharPosture"; + aSequence[4].Value = Integer.valueOf( aItalics.isSelected() ? 1 : 0 ); + aSequence[5] = new PropertyValue(); + aSequence[5].Name = "CharBackTransparent"; + aSequence[5].Value = Boolean.FALSE; + + bSuccess = xEdit.setAttributes( getSelectionStart(), + getSelectionEnd(), + aSequence ); + } + return bSuccess; + } + +} |