/* * 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 () { } public 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; } static String[] aTextActions = new String[] { "select...", "copy..." }; 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.show(); } } /** * Display a dialog with a text field and a pair of cancel/do-it buttons */ abstract class TextActionDialog extends JDialog implements ActionListener { AccTreeNode aNode; JTextArea aText; String sName; 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 ); } void cancel() { hide(); dispose(); } 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); } 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 */ 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 ); } class ColorIcon implements Icon { boolean bForeground; static final int nHeight = 16; 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; } }