import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeModelEvent; import java.util.Vector; import java.util.HashMap; import java.util.Enumeration; import java.util.LinkedList; import drafts.com.sun.star.accessibility.*; import com.sun.star.uno.UnoRuntime; import com.sun.star.uno.XInterface; import com.sun.star.uno.Any; import com.sun.star.lang.EventObject; import com.sun.star.lang.XServiceInfo; import com.sun.star.lang.XServiceName; public class AccessibilityTreeModel implements TreeModel, XAccessibleEventListener { // Map to translate from accessible object to corresponding tree node. protected HashMap maXAccessibleToNode; // Making both of these static is clearly a hack. protected static MessageInterface maMessageArea; protected static Print maPrinter; protected static boolean mbVerbose = true; // If the lock count is higher then zero, then no events are processed. private int mnLockCount; // The list of TreeModelListener objects. private Vector maTMListeners; // The root node of the tree. Use setRoot to change it. private AccessibleTreeNode maRoot = null; // default handlers private static Vector aDefaultHandlers; private static NodeHandler maContextHandler = new AccessibleContextHandler(); private static NodeHandler maTextHandler = new AccessibleTextHandler(); private static NodeHandler maEditableTextHandler = new AccessibleEditableTextHandler(); private static NodeHandler maComponentHandler = new AccessibleComponentHandler(); private static NodeHandler maExtendedComponentHandler = new AccessibleExtendedComponentHandler(); private static NodeHandler maActionHandler = new AccessibleActionHandler(); private static NodeHandler maImageHandler = new AccessibleImageHandler(); private static NodeHandler maTableHandler = new AccessibleTableHandler(); private static NodeHandler maCellHandler = new AccessibleCellHandler(); private static NodeHandler maHypertextHandler = new AccessibleHypertextHandler(); private static NodeHandler maHyperlinkHandler = new AccessibleHyperlinkHandler(); private static NodeHandler maSelectionHandler = new AccessibleSelectionHandler(); private static NodeHandler maRelationHandler = new AccessibleRelationHandler(); private static NodeHandler maTreeHandler = new AccessibleTreeHandler(); private static NodeHandler maUNOHandler = new AccessibleUNOHandler(); private Canvas maCanvas; public AccessibilityTreeModel (AccessibleTreeNode aRoot, MessageInterface aMessageArea, Print aPrinter) { // create default node (unless we have a 'proper' node) if( ! (aRoot instanceof AccessibleTreeNode) ) aRoot = new StringNode ("Root", null); setRoot (aRoot); maMessageArea = aMessageArea; maPrinter = aPrinter; maTMListeners = new Vector(); maXAccessibleToNode = new HashMap (); // syncronous or asyncronous event delivery? (i.e. same thread // or in s seperate event delivery thread) // xListener = this; // syncronous event delivery xListener = new QueuedListener(); // asyncronous event delivery } /** Lock the tree. While the tree is locked, events from the outside are not processed. Lock the tree when you change its internal structure. */ public void lock () { mnLockCount += 1; System.out.println ("locking: " + mnLockCount); } /** Unlock the tree. After unlocking the tree as many times as locking it, a treeStructureChange event is sent to the event listeners. @param aNodeHint If not null and treeStructureChange events are thrown then this node is used as root of the modified subtree. */ public void unlock (AccessibleTreeNode aNodeHint) { mnLockCount -= 1; System.out.println ("unlocking: " + mnLockCount); if (mnLockCount == 0) fireTreeStructureChanged ( new TreeModelEvent (this, new TreePath (aNodeHint.createPath()))); } /** Inform all listeners (especially the renderer) of a change of the tree's structure. @param aNode This node specifies the sub tree in which all changes take place. */ public void FireTreeStructureChanged (AccessibleTreeNode aNode) { } public Object getRoot() { return maRoot; } public synchronized void setRoot (AccessibleTreeNode aRoot) { if (maRoot == null) maRoot = aRoot; else { lock (); clear (); maRoot = aRoot; unlock (maRoot); } } /** Clears the model. That removes all nodes from the internal structures. */ public void clear () { System.out.println ("clearing the whole tree"); Object[] aNodes = maXAccessibleToNode.values().toArray(); for (int i=0; i 0) if ( ! removeChild (aNode.getChild (0))) break; // Remove node from its parent. AccessibleTreeNode aParent = aNode.getParent(); if (aParent != null) { int nIndex = aParent.indexOf(aNode); aParent.removeChild (nIndex); } removeNode (aNode); } } catch (Exception e) { System.out.println ("caught exception while removing child " + aNode + " : " + e); e.printStackTrace (); return false; } return true; } protected void removeNode (AccessibleTreeNode aNode) { try { if ((aNode != null) && (aNode instanceof AccTreeNode)) { // Remove node itself from internal data structures. removeFromCanvas ((AccTreeNode)aNode); removeAccListener ((AccTreeNode)aNode); maXAccessibleToNode.remove (((AccTreeNode)aNode).getAccessible()); } } catch (Exception e) { System.out.println ("caught exception while removing node " + aNode + " : " + e); e.printStackTrace(); } } /** Add add a new child to a parent. @return Returns the new or existing representation of the specified accessible object. */ protected AccessibleTreeNode addChild (AccTreeNode aParentNode, XAccessible xNewChild) { AccessibleTreeNode aChildNode = null; try { boolean bRet = false; // First make sure that the accessible object does not already have // a representation. aChildNode = (AccessibleTreeNode)maXAccessibleToNode.get (xNewChild); if (aChildNode == null) aChildNode = aParentNode.addAccessibleChild (xNewChild); else System.out.println ("node already present"); } catch (Exception e) { System.out.println ("caught exception while adding child " + xNewChild + " to parent " + aParentNode + ": " + e); e.printStackTrace (); } return aChildNode; } /** Add the child node to the internal tree structure. @param aNode The node to insert into the internal tree structure. */ protected boolean addNode (AccessibleTreeNode aNode) { boolean bRet = false; try { if (aNode instanceof AccTreeNode) { AccTreeNode aChild = (AccTreeNode)aNode; XAccessible xChild = aChild.getAccessible(); if (maXAccessibleToNode.get (xChild) == null) { registerAccListener (aChild); maXAccessibleToNode.put (xChild, aChild); addToCanvas (aChild); } bRet = true; } } catch (Exception e) { System.out.println ("caught exception while adding node " + aNode + ": " + e); e.printStackTrace (); } return bRet; } /** Return the tree node that is associated with the given accessible object. */ public AccessibleTreeNode getNode (XAccessible xAccessible) { return (AccessibleTreeNode)maXAccessibleToNode.get (xAccessible); } /** create path to node, suitable for TreeModelEvent constructor * @see javax.swing.event.TreeModelEvent#TreeModelEvent */ protected Object[] createPath (AccessibleTreeNode aNode) { Vector aPath = new Vector(); aNode.createPath (aPath); return aPath.toArray(); } // // listeners (and helper methods) // // We are registered with listeners as soon as objects are in the // tree cache, and we should get removed as soon as they are out. // public void addTreeModelListener(TreeModelListener l) { maTMListeners.add(l); } public void removeTreeModelListener(TreeModelListener l) { maTMListeners.remove(l); } protected void fireTreeNodesChanged(TreeModelEvent e) { System.out.println("treeNodesChanges: " + e); for(int i = 0; i < maTMListeners.size(); i++) { ((TreeModelListener)maTMListeners.get(i)).treeNodesChanged(e); } } protected void fireTreeNodesInserted(final TreeModelEvent e) { System.out.println("treeNodesInserted: " + e); for(int i = 0; i < maTMListeners.size(); i++) { ((TreeModelListener)maTMListeners.get(i)).treeNodesInserted(e); } } protected void fireTreeNodesRemoved(final TreeModelEvent e) { System.out.println("treeNodesRemoved: " + e); for(int i = 0; i < maTMListeners.size(); i++) { ((TreeModelListener)maTMListeners.get(i)).treeNodesRemoved(e); } } protected void fireTreeStructureChanged(final TreeModelEvent e) { System.out.println("treeStructureChanged: " + e); for(int i = 0; i < maTMListeners.size(); i++) { ((TreeModelListener)maTMListeners.get(i)).treeStructureChanged(e); } } protected TreeModelEvent createEvent (XAccessible xParent) { AccessibleTreeNode aParentNode = (AccessibleTreeNode)maXAccessibleToNode.get (xParent); return new TreeModelEvent (this, createPath (aParentNode)); } /** Create a TreeModelEvent object that informs listeners that one child has been removed from or inserted into its parent. */ public TreeModelEvent createEvent (XAccessible xParent, XAccessible xChild) { AccessibleTreeNode aParentNode = (AccessibleTreeNode)maXAccessibleToNode.get (xParent); return createEvent (aParentNode, xParent); } public TreeModelEvent createEvent (AccessibleTreeNode aParentNode, XAccessible xChild) { AccessibleTreeNode aChildNode = null; if (xChild != null) aChildNode = (AccessibleTreeNode)maXAccessibleToNode.get (xChild); return createEvent (aParentNode, aChildNode); } protected TreeModelEvent createEvent (AccessibleTreeNode aParentNode, AccessibleTreeNode aChildNode) { Object[] aPathToParent = createPath (aParentNode); int nIndexInParent = -1; if (aChildNode != null) nIndexInParent = aParentNode.indexOf (aChildNode); System.out.println (aChildNode + " " + nIndexInParent); if (nIndexInParent == -1) // This event may be passed only to treeStructureChanged of the listeners. return new TreeModelEvent (this, aPathToParent); else // General purpose event for removing or inserting known nodes. return new TreeModelEvent (this, aPathToParent, new int[] {nIndexInParent}, new Object[] {aChildNode} ); } /** Create a TreeModelEvent that indicates changes at those children of the specified node with the specified indices. */ protected TreeModelEvent createChangeEvent (AccTreeNode aNode, Vector aChildIndices) { // Build a list of child objects that are indicated by the given indices. int nCount = aChildIndices.size(); Object aChildObjects[] = new Object[nCount]; int nChildIndices[] = new int[nCount]; for (int i=0; i= aEventNames.length) ) nId = 0; System.out.println( "notify: " + aEvent.EventId + " " + aEventNames[nId] + ": [" + objectToString(aEvent.Source) + "] " + objectToString(aEvent.OldValue) + "->" + objectToString(aEvent.NewValue) ); if (mnLockCount > 0) { System.out.println ("ignoring event because tree is locked"); return; } XAccessible xSource = (XAccessible)UnoRuntime.queryInterface( XAccessible.class,aEvent.Source); switch( aEvent.EventId ) { case AccessibleEventId.ACCESSIBLE_CHILD_EVENT: // fire insertion and deletion events: if (aEvent.OldValue != null) { XAccessible xOld = (XAccessible)UnoRuntime.queryInterface( XAccessible.class,aEvent.OldValue); // Create event before removing the node to get the old // index of the node. TreeModelEvent aRemoveEvent = createEvent (xSource, xOld); removeChild ((AccessibleTreeNode)maXAccessibleToNode.get (xOld)); fireTreeNodesRemoved (aRemoveEvent); handleEvent (xSource, AccessibleTreeHandler.class); } // Insertion and removal of children should be mutually // exclusive. But then there is this 'should' ... if (aEvent.NewValue != null) { XAccessible xNew = (XAccessible)UnoRuntime.queryInterface( XAccessible.class,aEvent.NewValue); // Create event after inserting it so that its new index // in the parent can be determined. AccessibleTreeNode aParentNode = getNode (xSource); if (aParentNode instanceof AccTreeNode) { AccessibleTreeNode aChild = addChild ((AccTreeNode)aParentNode, xNew); if (addNode (aChild)) { // ((AccTreeNode)aParentNode).update (); updateOnCanvas ((AccTreeNode)aParentNode); // A call to fireTreeNodesInserted for xNew // should be sufficient but at least the // StringNode object that contains the number of // children also changes and we do not know its // index relative to its parent. Therefore the // more expensive fireTreeStructureChanged is // necessary. fireTreeNodesInserted (createEvent (xSource, xNew)); handleEvent (xSource, AccessibleTreeHandler.class); } } } maCanvas.repaint (); break; case AccessibleEventId.ACCESSIBLE_TABLE_MODEL_EVENT: AccessibleTableModelChange aModelChange = (AccessibleTableModelChange)aEvent.NewValue; System.out.println( "Range: StartRow " + aModelChange.FirstRow + " StartColumn " + aModelChange.FirstColumn + " EndRow " + aModelChange.LastRow + " EndColumn " + aModelChange.LastColumn + " Id " + aModelChange.Type); break; case AccessibleEventId.ACCESSIBLE_VISIBLE_DATA_EVENT: handleEvent (xSource, AccessibleComponentHandler.class, AccessibleExtendedComponentHandler.class); break; case AccessibleEventId.ACCESSIBLE_NAME_EVENT: case AccessibleEventId.ACCESSIBLE_DESCRIPTION_EVENT: case AccessibleEventId.ACCESSIBLE_STATE_EVENT: case AccessibleEventId.CONTROLLED_BY_EVENT: case AccessibleEventId.CONTROLLER_FOR_EVENT: case AccessibleEventId.LABEL_FOR_EVENT: case AccessibleEventId.LABELED_BY_EVENT: case AccessibleEventId.MEMBER_OF_EVENT: case AccessibleEventId.ACCESSIBLE_SELECTION_EVENT: handleEvent (xSource, AccessibleContextHandler.class); break; case AccessibleEventId.ACCESSIBLE_TABLE_CAPTION_EVENT: case AccessibleEventId.ACCESSIBLE_TABLE_COLUMN_DESCRIPTION_EVENT: case AccessibleEventId.ACCESSIBLE_TABLE_COLUMN_HEADER_EVENT: case AccessibleEventId.ACCESSIBLE_TABLE_ROW_DESCRIPTION_EVENT: case AccessibleEventId.ACCESSIBLE_TABLE_ROW_HEADER_EVENT: case AccessibleEventId.ACCESSIBLE_TABLE_SUMMARY_EVENT: handleEvent (xSource, AccessibleTableHandler.class); break; case AccessibleEventId.ACCESSIBLE_ACTION_EVENT: handleEvent (xSource, AccessibleActionHandler.class); break; case AccessibleEventId.ACCESSIBLE_HYPERTEXT_EVENT: handleEvent (xSource, AccessibleHypertextHandler.class); break; case AccessibleEventId.ACCESSIBLE_ACTIVE_DESCENDANT_EVENT: case AccessibleEventId.ACCESSIBLE_CARET_EVENT: case AccessibleEventId.ACCESSIBLE_TEXT_EVENT: case AccessibleEventId.ACCESSIBLE_VALUE_EVENT: handleEvent (xSource, AccessibleTextHandler.class); break; default: break; } } // // canvas // public void setCanvas( Canvas aCanvas ) { maCanvas = aCanvas; } protected void addToCanvas (AccTreeNode aNode) { if (maCanvas != null) maCanvas.addNode (aNode); } protected void removeFromCanvas (AccTreeNode aNode) { if (maCanvas != null) maCanvas.removeNode (aNode); } protected void updateOnCanvas (AccTreeNode aNode) { if (maCanvas != null) maCanvas.updateNode (aNode); } /** QueuedListener implements an AccessibleEventListener which * delegates all events to another such listener, but does so in a * seperate thread */ class QueuedListener implements XAccessibleEventListener, Runnable { public QueuedListener() { System.out.println ("starting new queued listener"); // initiate thread new Thread(this, "QueuedListener").start(); } /** The queue of event objects, LinkedList * The queue object will also serve as lock for the * consumer/producer type syncronization. */ protected LinkedList aQueue = new LinkedList(); /// This thread's main method: deliver all events public void run() { // in an infinite loop, check for events to deliver, then // wait on lock (which will be notified when new events arrive) while( true ) { Runnable aEvent = null; do { synchronized( aQueue ) { aEvent = (aQueue.size() > 0) ? (Runnable)aQueue.removeFirst() : null; } if( aEvent != null ) { System.out.println("Deliver event: " + aEvent.hashCode()); try { aEvent.run(); } catch( Throwable e ) { System.out.println( "Exception during event delivery: " + e ); e.printStackTrace(); } } } while( aEvent != null ); try { synchronized( aQueue ) { aQueue.wait(); } } catch( Exception e ) { // can't wait? odd! System.err.println("Can't wait!"); e.printStackTrace(); } } } public void disposing( final EventObject aEvent) { System.out.println( "Queue disposing: " + aEvent.hashCode() ); synchronized( aQueue ) { aQueue.addLast( new Runnable() { public void run() { AccessibilityTreeModel.this.disposing( aEvent ); } public int hashCode() { return aEvent.hashCode(); } } ); aQueue.notify(); } } public void notifyEvent( final AccessibleEventObject aEvent ) { System.out.println( "Queue notifyEvent: " + aEvent.hashCode() ); synchronized( aQueue ) { aQueue.addLast( new Runnable() { public void run() { AccessibilityTreeModel.this.notifyEvent( aEvent ); } public int hashCode() { return aEvent.hashCode(); } } ); aQueue.notify(); } } } }