/* * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing.text.html; import java.awt.*; import java.awt.event.*; import java.beans.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import javax.accessibility.*; import java.text.BreakIterator; /* * The AccessibleHTML class provide information about the contents * of a HTML document to assistive technologies. * * @author Lynn Monsanto */ class AccessibleHTML implements Accessible { /** * The editor. */ private JEditorPane editor; /** * Current model. */ private Document model; /** * DocumentListener installed on the current model. */ private DocumentListener docListener; /** * PropertyChangeListener installed on the editor */ private PropertyChangeListener propChangeListener; /** * The root ElementInfo for the document */ private ElementInfo rootElementInfo; /* * The root accessible context for the document */ private RootHTMLAccessibleContext rootHTMLAccessibleContext; public AccessibleHTML(JEditorPane pane) { editor = pane; propChangeListener = new PropertyChangeHandler(); setDocument(editor.getDocument()); docListener = new DocumentHandler(); } /** * Sets the document. */ private void setDocument(Document document) { if (model != null) { model.removeDocumentListener(docListener); } if (editor != null) { editor.removePropertyChangeListener(propChangeListener); } this.model = document; if (model != null) { if (rootElementInfo != null) { rootElementInfo.invalidate(false); } buildInfo(); model.addDocumentListener(docListener); } else { rootElementInfo = null; } if (editor != null) { editor.addPropertyChangeListener(propChangeListener); } } /** * Returns the Document currently presenting information for. */ private Document getDocument() { return model; } /** * Returns the JEditorPane providing information for. */ private JEditorPane getTextComponent() { return editor; } /** * Returns the ElementInfo representing the root Element. */ private ElementInfo getRootInfo() { return rootElementInfo; } /** * Returns the root <code>View</code> associated with the current text * component. */ private View getRootView() { return getTextComponent().getUI().getRootView(getTextComponent()); } /** * Returns the bounds the root View will be rendered in. */ private Rectangle getRootEditorRect() { Rectangle alloc = getTextComponent().getBounds(); if ((alloc.width > 0) && (alloc.height > 0)) { alloc.x = alloc.y = 0; Insets insets = editor.getInsets(); alloc.x += insets.left; alloc.y += insets.top; alloc.width -= insets.left + insets.right; alloc.height -= insets.top + insets.bottom; return alloc; } return null; } /** * If possible acquires a lock on the Document. If a lock has been * obtained a key will be retured that should be passed to * <code>unlock</code>. */ private Object lock() { Document document = getDocument(); if (document instanceof AbstractDocument) { ((AbstractDocument)document).readLock(); return document; } return null; } /** * Releases a lock previously obtained via <code>lock</code>. */ private void unlock(Object key) { if (key != null) { ((AbstractDocument)key).readUnlock(); } } /** * Rebuilds the information from the current info. */ private void buildInfo() { Object lock = lock(); try { Document doc = getDocument(); Element root = doc.getDefaultRootElement(); rootElementInfo = new ElementInfo(root); rootElementInfo.validate(); } finally { unlock(lock); } } /* * Create an ElementInfo subclass based on the passed in Element. */ ElementInfo createElementInfo(Element e, ElementInfo parent) { AttributeSet attrs = e.getAttributes(); if (attrs != null) { Object name = attrs.getAttribute(StyleConstants.NameAttribute); if (name == HTML.Tag.IMG) { return new IconElementInfo(e, parent); } else if (name == HTML.Tag.CONTENT || name == HTML.Tag.CAPTION) { return new TextElementInfo(e, parent); } else if (name == HTML.Tag.TABLE) { return new TableElementInfo(e, parent); } } return null; } /** * Returns the root AccessibleContext for the document */ public AccessibleContext getAccessibleContext() { if (rootHTMLAccessibleContext == null) { rootHTMLAccessibleContext = new RootHTMLAccessibleContext(rootElementInfo); } return rootHTMLAccessibleContext; } /* * The roow AccessibleContext for the document */ private class RootHTMLAccessibleContext extends HTMLAccessibleContext { public RootHTMLAccessibleContext(ElementInfo elementInfo) { super(elementInfo); } /** * Gets the accessibleName property of this object. The accessibleName * property of an object is a localized String that designates the purpose * of the object. For example, the accessibleName property of a label * or button might be the text of the label or button itself. In the * case of an object that doesn't display its name, the accessibleName * should still be set. For example, in the case of a text field used * to enter the name of a city, the accessibleName for the en_US locale * could be 'city.' * * @return the localized name of the object; null if this * object does not have a name * * @see #setAccessibleName */ public String getAccessibleName() { if (model != null) { return (String)model.getProperty(Document.TitleProperty); } else { return null; } } /** * Gets the accessibleDescription property of this object. If this * property isn't set, returns the content type of this * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text"). * * @return the localized description of the object; <code>null</code> * if this object does not have a description * * @see #setAccessibleName */ public String getAccessibleDescription() { return editor.getContentType(); } /** * Gets the role of this object. The role of the object is the generic * purpose or use of the class of this object. For example, the role * of a push button is AccessibleRole.PUSH_BUTTON. The roles in * AccessibleRole are provided so component developers can pick from * a set of predefined roles. This enables assistive technologies to * provide a consistent interface to various tweaked subclasses of * components (e.g., use AccessibleRole.PUSH_BUTTON for all components * that act like a push button) as well as distinguish between subclasses * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes * and AccessibleRole.RADIO_BUTTON for radio buttons). * <p>Note that the AccessibleRole class is also extensible, so * custom component developers can define their own AccessibleRole's * if the set of predefined roles is inadequate. * * @return an instance of AccessibleRole describing the role of the object * @see AccessibleRole */ public AccessibleRole getAccessibleRole() { return AccessibleRole.TEXT; } } /* * Base AccessibleContext class for HTML elements */ protected abstract class HTMLAccessibleContext extends AccessibleContext implements Accessible, AccessibleComponent { protected ElementInfo elementInfo; public HTMLAccessibleContext(ElementInfo elementInfo) { this.elementInfo = elementInfo; } // begin AccessibleContext implementation ... public AccessibleContext getAccessibleContext() { return this; } /** * Gets the state set of this object. * * @return an instance of AccessibleStateSet describing the states * of the object * @see AccessibleStateSet */ public AccessibleStateSet getAccessibleStateSet() { AccessibleStateSet states = new AccessibleStateSet(); Component comp = getTextComponent(); if (comp.isEnabled()) { states.add(AccessibleState.ENABLED); } if (comp instanceof JTextComponent && ((JTextComponent)comp).isEditable()) { states.add(AccessibleState.EDITABLE); states.add(AccessibleState.FOCUSABLE); } if (comp.isVisible()) { states.add(AccessibleState.VISIBLE); } if (comp.isShowing()) { states.add(AccessibleState.SHOWING); } return states; } /** * Gets the 0-based index of this object in its accessible parent. * * @return the 0-based index of this object in its parent; -1 if this * object does not have an accessible parent. * * @see #getAccessibleParent * @see #getAccessibleChildrenCount * @see #getAccessibleChild */ public int getAccessibleIndexInParent() { return elementInfo.getIndexInParent(); } /** * Returns the number of accessible children of the object. * * @return the number of accessible children of the object. */ public int getAccessibleChildrenCount() { return elementInfo.getChildCount(); } /** * Returns the specified Accessible child of the object. The Accessible * children of an Accessible object are zero-based, so the first child * of an Accessible child is at index 0, the second child is at index 1, * and so on. * * @param i zero-based index of child * @return the Accessible child of the object * @see #getAccessibleChildrenCount */ public Accessible getAccessibleChild(int i) { ElementInfo childInfo = elementInfo.getChild(i); if (childInfo != null && childInfo instanceof Accessible) { return (Accessible)childInfo; } else { return null; } } /** * Gets the locale of the component. If the component does not have a * locale, then the locale of its parent is returned. * * @return this component's locale. If this component does not have * a locale, the locale of its parent is returned. * * @exception IllegalComponentStateException * If the Component does not have its own locale and has not yet been * added to a containment hierarchy such that the locale can be * determined from the containing parent. */ public Locale getLocale() throws IllegalComponentStateException { return editor.getLocale(); } // ... end AccessibleContext implementation // begin AccessibleComponent implementation ... public AccessibleComponent getAccessibleComponent() { return this; } /** * Gets the background color of this object. * * @return the background color, if supported, of the object; * otherwise, null * @see #setBackground */ public Color getBackground() { return getTextComponent().getBackground(); } /** * Sets the background color of this object. * * @param c the new Color for the background * @see #setBackground */ public void setBackground(Color c) { getTextComponent().setBackground(c); } /** * Gets the foreground color of this object. * * @return the foreground color, if supported, of the object; * otherwise, null * @see #setForeground */ public Color getForeground() { return getTextComponent().getForeground(); } /** * Sets the foreground color of this object. * * @param c the new Color for the foreground * @see #getForeground */ public void setForeground(Color c) { getTextComponent().setForeground(c); } /** * Gets the Cursor of this object. * * @return the Cursor, if supported, of the object; otherwise, null * @see #setCursor */ public Cursor getCursor() { return getTextComponent().getCursor(); } /** * Sets the Cursor of this object. * * @param cursor the new Cursor for the object * @see #getCursor */ public void setCursor(Cursor cursor) { getTextComponent().setCursor(cursor); } /** * Gets the Font of this object. * * @return the Font,if supported, for the object; otherwise, null * @see #setFont */ public Font getFont() { return getTextComponent().getFont(); } /** * Sets the Font of this object. * * @param f the new Font for the object * @see #getFont */ public void setFont(Font f) { getTextComponent().setFont(f); } /** * Gets the FontMetrics of this object. * * @param f the Font * @return the FontMetrics, if supported, the object; otherwise, null * @see #getFont */ public FontMetrics getFontMetrics(Font f) { return getTextComponent().getFontMetrics(f); } /** * Determines if the object is enabled. Objects that are enabled * will also have the AccessibleState.ENABLED state set in their * AccessibleStateSets. * * @return true if object is enabled; otherwise, false * @see #setEnabled * @see AccessibleContext#getAccessibleStateSet * @see AccessibleState#ENABLED * @see AccessibleStateSet */ public boolean isEnabled() { return getTextComponent().isEnabled(); } /** * Sets the enabled state of the object. * * @param b if true, enables this object; otherwise, disables it * @see #isEnabled */ public void setEnabled(boolean b) { getTextComponent().setEnabled(b); } /** * Determines if the object is visible. Note: this means that the * object intends to be visible; however, it may not be * showing on the screen because one of the objects that this object * is contained by is currently not visible. To determine if an object * is showing on the screen, use isShowing(). * <p>Objects that are visible will also have the * AccessibleState.VISIBLE state set in their AccessibleStateSets. * * @return true if object is visible; otherwise, false * @see #setVisible * @see AccessibleContext#getAccessibleStateSet * @see AccessibleState#VISIBLE * @see AccessibleStateSet */ public boolean isVisible() { return getTextComponent().isVisible(); } /** * Sets the visible state of the object. * * @param b if true, shows this object; otherwise, hides it * @see #isVisible */ public void setVisible(boolean b) { getTextComponent().setVisible(b); } /** * Determines if the object is showing. This is determined by checking * the visibility of the object and its ancestors. * Note: this * will return true even if the object is obscured by another (for * example, it is underneath a menu that was pulled down). * * @return true if object is showing; otherwise, false */ public boolean isShowing() { return getTextComponent().isShowing(); } /** * Checks whether the specified point is within this object's bounds, * where the point's x and y coordinates are defined to be relative * to the coordinate system of the object. * * @param p the Point relative to the coordinate system of the object * @return true if object contains Point; otherwise false * @see #getBounds */ public boolean contains(Point p) { Rectangle r = getBounds(); if (r != null) { return r.contains(p.x, p.y); } else { return false; } } /** * Returns the location of the object on the screen. * * @return the location of the object on screen; null if this object * is not on the screen * @see #getBounds * @see #getLocation */ public Point getLocationOnScreen() { Point editorLocation = getTextComponent().getLocationOnScreen(); Rectangle r = getBounds(); if (r != null) { return new Point(editorLocation.x + r.x, editorLocation.y + r.y); } else { return null; } } /** * Gets the location of the object relative to the parent in the form * of a point specifying the object's top-left corner in the screen's * coordinate space. * * @return An instance of Point representing the top-left corner of the * object's bounds in the coordinate space of the screen; null if * this object or its parent are not on the screen * @see #getBounds * @see #getLocationOnScreen */ public Point getLocation() { Rectangle r = getBounds(); if (r != null) { return new Point(r.x, r.y); } else { return null; } } /** * Sets the location of the object relative to the parent. * @param p the new position for the top-left corner * @see #getLocation */ public void setLocation(Point p) { } /** * Gets the bounds of this object in the form of a Rectangle object. * The bounds specify this object's width, height, and location * relative to its parent. * * @return A rectangle indicating this component's bounds; null if * this object is not on the screen. * @see #contains */ public Rectangle getBounds() { return elementInfo.getBounds(); } /** * Sets the bounds of this object in the form of a Rectangle object. * The bounds specify this object's width, height, and location * relative to its parent. * * @param r rectangle indicating this component's bounds * @see #getBounds */ public void setBounds(Rectangle r) { } /** * Returns the size of this object in the form of a Dimension object. * The height field of the Dimension object contains this object's * height, and the width field of the Dimension object contains this * object's width. * * @return A Dimension object that indicates the size of this component; * null if this object is not on the screen * @see #setSize */ public Dimension getSize() { Rectangle r = getBounds(); if (r != null) { return new Dimension(r.width, r.height); } else { return null; } } /** * Resizes this object so that it has width and height. * * @param d The dimension specifying the new size of the object. * @see #getSize */ public void setSize(Dimension d) { Component comp = getTextComponent(); comp.setSize(d); } /** * Returns the Accessible child, if one exists, contained at the local * coordinate Point. * * @param p The point relative to the coordinate system of this object. * @return the Accessible, if it exists, at the specified location; * otherwise null */ public Accessible getAccessibleAt(Point p) { ElementInfo innerMostElement = getElementInfoAt(rootElementInfo, p); if (innerMostElement instanceof Accessible) { return (Accessible)innerMostElement; } else { return null; } } private ElementInfo getElementInfoAt(ElementInfo elementInfo, Point p) { if (elementInfo.getBounds() == null) { return null; } if (elementInfo.getChildCount() == 0 && elementInfo.getBounds().contains(p)) { return elementInfo; } else { if (elementInfo instanceof TableElementInfo) { // Handle table caption as a special case since it's the // only table child that is not a table row. ElementInfo captionInfo = ((TableElementInfo)elementInfo).getCaptionInfo(); if (captionInfo != null) { Rectangle bounds = captionInfo.getBounds(); if (bounds != null && bounds.contains(p)) { return captionInfo; } } } for (int i = 0; i < elementInfo.getChildCount(); i++) { ElementInfo childInfo = elementInfo.getChild(i); ElementInfo retValue = getElementInfoAt(childInfo, p); if (retValue != null) { return retValue; } } } return null; } /** * Returns whether this object can accept focus or not. Objects that * can accept focus will also have the AccessibleState.FOCUSABLE state * set in their AccessibleStateSets. * * @return true if object can accept focus; otherwise false * @see AccessibleContext#getAccessibleStateSet * @see AccessibleState#FOCUSABLE * @see AccessibleState#FOCUSED * @see AccessibleStateSet */ public boolean isFocusTraversable() { Component comp = getTextComponent(); if (comp instanceof JTextComponent) { if (((JTextComponent)comp).isEditable()) { return true; } } return false; } /** * Requests focus for this object. If this object cannot accept focus, * nothing will happen. Otherwise, the object will attempt to take * focus. * @see #isFocusTraversable */ public void requestFocus() { // TIGER - 4856191 if (! isFocusTraversable()) { return; } Component comp = getTextComponent(); if (comp instanceof JTextComponent) { comp.requestFocusInWindow(); try { if (elementInfo.validateIfNecessary()) { // set the caret position to the start of this component Element elem = elementInfo.getElement(); ((JTextComponent)comp).setCaretPosition(elem.getStartOffset()); // fire a AccessibleState.FOCUSED property change event AccessibleContext ac = editor.getAccessibleContext(); PropertyChangeEvent pce = new PropertyChangeEvent(this, AccessibleContext.ACCESSIBLE_STATE_PROPERTY, null, AccessibleState.FOCUSED); ac.firePropertyChange( AccessibleContext.ACCESSIBLE_STATE_PROPERTY, null, pce); } } catch (IllegalArgumentException e) { // don't fire property change event } } } /** * Adds the specified focus listener to receive focus events from this * component. * * @param l the focus listener * @see #removeFocusListener */ public void addFocusListener(FocusListener l) { getTextComponent().addFocusListener(l); } /** * Removes the specified focus listener so it no longer receives focus * events from this component. * * @param l the focus listener * @see #addFocusListener */ public void removeFocusListener(FocusListener l) { getTextComponent().removeFocusListener(l); } // ... end AccessibleComponent implementation } // ... end HTMLAccessibleContext /* * ElementInfo for text */ class TextElementInfo extends ElementInfo implements Accessible { TextElementInfo(Element element, ElementInfo parent) { super(element, parent); } // begin AccessibleText implementation ... private AccessibleContext accessibleContext; public AccessibleContext getAccessibleContext() { if (accessibleContext == null) { accessibleContext = new TextAccessibleContext(this); } return accessibleContext; } /* * AccessibleContext for text elements */ public class TextAccessibleContext extends HTMLAccessibleContext implements AccessibleText { public TextAccessibleContext(ElementInfo elementInfo) { super(elementInfo); } public AccessibleText getAccessibleText() { return this; } /** * Gets the accessibleName property of this object. The accessibleName * property of an object is a localized String that designates the purpose * of the object. For example, the accessibleName property of a label * or button might be the text of the label or button itself. In the * case of an object that doesn't display its name, the accessibleName * should still be set. For example, in the case of a text field used * to enter the name of a city, the accessibleName for the en_US locale * could be 'city.' * * @return the localized name of the object; null if this * object does not have a name * * @see #setAccessibleName */ public String getAccessibleName() { if (model != null) { return (String)model.getProperty(Document.TitleProperty); } else { return null; } } /** * Gets the accessibleDescription property of this object. If this * property isn't set, returns the content type of this * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text"). * * @return the localized description of the object; <code>null</code> * if this object does not have a description * * @see #setAccessibleName */ public String getAccessibleDescription() { return editor.getContentType(); } /** * Gets the role of this object. The role of the object is the generic * purpose or use of the class of this object. For example, the role * of a push button is AccessibleRole.PUSH_BUTTON. The roles in * AccessibleRole are provided so component developers can pick from * a set of predefined roles. This enables assistive technologies to * provide a consistent interface to various tweaked subclasses of * components (e.g., use AccessibleRole.PUSH_BUTTON for all components * that act like a push button) as well as distinguish between subclasses * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes * and AccessibleRole.RADIO_BUTTON for radio buttons). * <p>Note that the AccessibleRole class is also extensible, so * custom component developers can define their own AccessibleRole's * if the set of predefined roles is inadequate. * * @return an instance of AccessibleRole describing the role of the object * @see AccessibleRole */ public AccessibleRole getAccessibleRole() { return AccessibleRole.TEXT; } /** * Given a point in local coordinates, return the zero-based index * of the character under that Point. If the point is invalid, * this method returns -1. * * @param p the Point in local coordinates * @return the zero-based index of the character under Point p; if * Point is invalid returns -1. */ @SuppressWarnings("deprecation") public int getIndexAtPoint(Point p) { View v = getView(); if (v != null) { return v.viewToModel(p.x, p.y, getBounds()); } else { return -1; } } /** * Determine the bounding box of the character at the given * index into the string. The bounds are returned in local * coordinates. If the index is invalid an empty rectangle is * returned. * * @param i the index into the String * @return the screen coordinates of the character's the bounding box, * if index is invalid returns an empty rectangle. */ @SuppressWarnings("deprecation") public Rectangle getCharacterBounds(int i) { try { return editor.getUI().modelToView(editor, i); } catch (BadLocationException e) { return null; } } /** * Return the number of characters (valid indicies) * * @return the number of characters */ public int getCharCount() { if (validateIfNecessary()) { Element elem = elementInfo.getElement(); return elem.getEndOffset() - elem.getStartOffset(); } return 0; } /** * Return the zero-based offset of the caret. * * Note: That to the right of the caret will have the same index * value as the offset (the caret is between two characters). * @return the zero-based offset of the caret. */ public int getCaretPosition() { View v = getView(); if (v == null) { return -1; } Container c = v.getContainer(); if (c == null) { return -1; } if (c instanceof JTextComponent) { return ((JTextComponent)c).getCaretPosition(); } else { return -1; } } /** * IndexedSegment extends Segment adding the offset into the * the model the <code>Segment</code> was asked for. */ private class IndexedSegment extends Segment { /** * Offset into the model that the position represents. */ public int modelOffset; } public String getAtIndex(int part, int index) { return getAtIndex(part, index, 0); } public String getAfterIndex(int part, int index) { return getAtIndex(part, index, 1); } public String getBeforeIndex(int part, int index) { return getAtIndex(part, index, -1); } /** * Gets the word, sentence, or character at <code>index</code>. * If <code>direction</code> is non-null this will find the * next/previous word/sentence/character. */ private String getAtIndex(int part, int index, int direction) { if (model instanceof AbstractDocument) { ((AbstractDocument)model).readLock(); } try { if (index < 0 || index >= model.getLength()) { return null; } switch (part) { case AccessibleText.CHARACTER: if (index + direction < model.getLength() && index + direction >= 0) { return model.getText(index + direction, 1); } break; case AccessibleText.WORD: case AccessibleText.SENTENCE: IndexedSegment seg = getSegmentAt(part, index); if (seg != null) { if (direction != 0) { int next; if (direction < 0) { next = seg.modelOffset - 1; } else { next = seg.modelOffset + direction * seg.count; } if (next >= 0 && next <= model.getLength()) { seg = getSegmentAt(part, next); } else { seg = null; } } if (seg != null) { return new String(seg.array, seg.offset, seg.count); } } break; default: break; } } catch (BadLocationException e) { } finally { if (model instanceof AbstractDocument) { ((AbstractDocument)model).readUnlock(); } } return null; } /* * Returns the paragraph element for the specified index. */ private Element getParagraphElement(int index) { if (model instanceof PlainDocument ) { PlainDocument sdoc = (PlainDocument)model; return sdoc.getParagraphElement(index); } else if (model instanceof StyledDocument) { StyledDocument sdoc = (StyledDocument)model; return sdoc.getParagraphElement(index); } else { Element para; for (para = model.getDefaultRootElement(); ! para.isLeaf(); ) { int pos = para.getElementIndex(index); para = para.getElement(pos); } if (para == null) { return null; } return para.getParentElement(); } } /* * Returns a <code>Segment</code> containing the paragraph text * at <code>index</code>, or null if <code>index</code> isn't * valid. */ private IndexedSegment getParagraphElementText(int index) throws BadLocationException { Element para = getParagraphElement(index); if (para != null) { IndexedSegment segment = new IndexedSegment(); try { int length = para.getEndOffset() - para.getStartOffset(); model.getText(para.getStartOffset(), length, segment); } catch (BadLocationException e) { return null; } segment.modelOffset = para.getStartOffset(); return segment; } return null; } /** * Returns the Segment at <code>index</code> representing either * the paragraph or sentence as identified by <code>part</code>, or * null if a valid paragraph/sentence can't be found. The offset * will point to the start of the word/sentence in the array, and * the modelOffset will point to the location of the word/sentence * in the model. */ private IndexedSegment getSegmentAt(int part, int index) throws BadLocationException { IndexedSegment seg = getParagraphElementText(index); if (seg == null) { return null; } BreakIterator iterator; switch (part) { case AccessibleText.WORD: iterator = BreakIterator.getWordInstance(getLocale()); break; case AccessibleText.SENTENCE: iterator = BreakIterator.getSentenceInstance(getLocale()); break; default: return null; } seg.first(); iterator.setText(seg); int end = iterator.following(index - seg.modelOffset + seg.offset); if (end == BreakIterator.DONE) { return null; } if (end > seg.offset + seg.count) { return null; } int begin = iterator.previous(); if (begin == BreakIterator.DONE || begin >= seg.offset + seg.count) { return null; } seg.modelOffset = seg.modelOffset + begin - seg.offset; seg.offset = begin; seg.count = end - begin; return seg; } /** * Return the AttributeSet for a given character at a given index * * @param i the zero-based index into the text * @return the AttributeSet of the character */ public AttributeSet getCharacterAttribute(int i) { if (model instanceof StyledDocument) { StyledDocument doc = (StyledDocument)model; Element elem = doc.getCharacterElement(i); if (elem != null) { return elem.getAttributes(); } } return null; } /** * Returns the start offset within the selected text. * If there is no selection, but there is * a caret, the start and end offsets will be the same. * * @return the index into the text of the start of the selection */ public int getSelectionStart() { return editor.getSelectionStart(); } /** * Returns the end offset within the selected text. * If there is no selection, but there is * a caret, the start and end offsets will be the same. * * @return the index into the text of the end of the selection */ public int getSelectionEnd() { return editor.getSelectionEnd(); } /** * Returns the portion of the text that is selected. * * @return the String portion of the text that is selected */ public String getSelectedText() { return editor.getSelectedText(); } /* * Returns the text substring starting at the specified * offset with the specified length. */ private String getText(int offset, int length) throws BadLocationException { if (model != null && model instanceof StyledDocument) { StyledDocument doc = (StyledDocument)model; return model.getText(offset, length); } else { return null; } } } } /* * ElementInfo for images */ private class IconElementInfo extends ElementInfo implements Accessible { private int width = -1; private int height = -1; IconElementInfo(Element element, ElementInfo parent) { super(element, parent); } protected void invalidate(boolean first) { super.invalidate(first); width = height = -1; } private int getImageSize(Object key) { if (validateIfNecessary()) { int size = getIntAttr(getAttributes(), key, -1); if (size == -1) { View v = getView(); size = 0; if (v instanceof ImageView) { Image img = ((ImageView)v).getImage(); if (img != null) { if (key == HTML.Attribute.WIDTH) { size = img.getWidth(null); } else { size = img.getHeight(null); } } } } return size; } return 0; } // begin AccessibleIcon implementation ... private AccessibleContext accessibleContext; public AccessibleContext getAccessibleContext() { if (accessibleContext == null) { accessibleContext = new IconAccessibleContext(this); } return accessibleContext; } /* * AccessibleContext for images */ protected class IconAccessibleContext extends HTMLAccessibleContext implements AccessibleIcon { public IconAccessibleContext(ElementInfo elementInfo) { super(elementInfo); } /** * Gets the accessibleName property of this object. The accessibleName * property of an object is a localized String that designates the purpose * of the object. For example, the accessibleName property of a label * or button might be the text of the label or button itself. In the * case of an object that doesn't display its name, the accessibleName * should still be set. For example, in the case of a text field used * to enter the name of a city, the accessibleName for the en_US locale * could be 'city.' * * @return the localized name of the object; null if this * object does not have a name * * @see #setAccessibleName */ public String getAccessibleName() { return getAccessibleIconDescription(); } /** * Gets the accessibleDescription property of this object. If this * property isn't set, returns the content type of this * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text"). * * @return the localized description of the object; <code>null</code> * if this object does not have a description * * @see #setAccessibleName */ public String getAccessibleDescription() { return editor.getContentType(); } /** * Gets the role of this object. The role of the object is the generic * purpose or use of the class of this object. For example, the role * of a push button is AccessibleRole.PUSH_BUTTON. The roles in * AccessibleRole are provided so component developers can pick from * a set of predefined roles. This enables assistive technologies to * provide a consistent interface to various tweaked subclasses of * components (e.g., use AccessibleRole.PUSH_BUTTON for all components * that act like a push button) as well as distinguish between subclasses * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes * and AccessibleRole.RADIO_BUTTON for radio buttons). * <p>Note that the AccessibleRole class is also extensible, so * custom component developers can define their own AccessibleRole's * if the set of predefined roles is inadequate. * * @return an instance of AccessibleRole describing the role of the object * @see AccessibleRole */ public AccessibleRole getAccessibleRole() { return AccessibleRole.ICON; } public AccessibleIcon [] getAccessibleIcon() { AccessibleIcon [] icons = new AccessibleIcon[1]; icons[0] = this; return icons; } /** * Gets the description of the icon. This is meant to be a brief * textual description of the object. For example, it might be * presented to a blind user to give an indication of the purpose * of the icon. * * @return the description of the icon */ public String getAccessibleIconDescription() { return ((ImageView)getView()).getAltText(); } /** * Sets the description of the icon. This is meant to be a brief * textual description of the object. For example, it might be * presented to a blind user to give an indication of the purpose * of the icon. * * @param description the description of the icon */ public void setAccessibleIconDescription(String description) { } /** * Gets the width of the icon * * @return the width of the icon. */ public int getAccessibleIconWidth() { if (width == -1) { width = getImageSize(HTML.Attribute.WIDTH); } return width; } /** * Gets the height of the icon * * @return the height of the icon. */ public int getAccessibleIconHeight() { if (height == -1) { height = getImageSize(HTML.Attribute.HEIGHT); } return height; } } // ... end AccessibleIconImplementation } /** * TableElementInfo encapsulates information about a HTML.Tag.TABLE. * To make access fast it crates a grid containing the children to * allow for access by row, column. TableElementInfo will contain * TableRowElementInfos, which will contain TableCellElementInfos. * Any time one of the rows or columns becomes invalid the table is * invalidated. This is because any time one of the child attributes * changes the size of the grid may have changed. */ private class TableElementInfo extends ElementInfo implements Accessible { protected ElementInfo caption; /** * Allocation of the table by row x column. There may be holes (eg * nulls) depending upon the html, any cell that has a rowspan/colspan * > 1 will be contained multiple times in the grid. */ private TableCellElementInfo[][] grid; TableElementInfo(Element e, ElementInfo parent) { super(e, parent); } public ElementInfo getCaptionInfo() { return caption; } /** * Overriden to update the grid when validating. */ protected void validate() { super.validate(); updateGrid(); } /** * Overriden to only alloc instances of TableRowElementInfos. */ protected void loadChildren(Element e) { for (int counter = 0; counter < e.getElementCount(); counter++) { Element child = e.getElement(counter); AttributeSet attrs = child.getAttributes(); if (attrs.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.TR) { addChild(new TableRowElementInfo(child, this, counter)); } else if (attrs.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.CAPTION) { // Handle captions as a special case since all other // children are table rows. caption = createElementInfo(child, this); } } } /** * Updates the grid. */ private void updateGrid() { // Determine the max row/col count. int delta = 0; int maxCols = 0; int rows; for (int counter = 0; counter < getChildCount(); counter++) { TableRowElementInfo row = getRow(counter); int prev = 0; for (int y = 0; y < delta; y++) { prev = Math.max(prev, getRow(counter - y - 1). getColumnCount(y + 2)); } delta = Math.max(row.getRowCount(), delta); delta--; maxCols = Math.max(maxCols, row.getColumnCount() + prev); } rows = getChildCount() + delta; // Alloc grid = new TableCellElementInfo[rows][]; for (int counter = 0; counter < rows; counter++) { grid[counter] = new TableCellElementInfo[maxCols]; } // Update for (int counter = 0; counter < rows; counter++) { getRow(counter).updateGrid(counter); } } /** * Returns the TableCellElementInfo at the specified index. */ public TableRowElementInfo getRow(int index) { return (TableRowElementInfo)getChild(index); } /** * Returns the TableCellElementInfo by row and column. */ public TableCellElementInfo getCell(int r, int c) { if (validateIfNecessary() && r < grid.length && c < grid[0].length) { return grid[r][c]; } return null; } /** * Returns the rowspan of the specified entry. */ public int getRowExtentAt(int r, int c) { TableCellElementInfo cell = getCell(r, c); if (cell != null) { int rows = cell.getRowCount(); int delta = 1; while ((r - delta) >= 0 && grid[r - delta][c] == cell) { delta++; } return rows - delta + 1; } return 0; } /** * Returns the colspan of the specified entry. */ public int getColumnExtentAt(int r, int c) { TableCellElementInfo cell = getCell(r, c); if (cell != null) { int cols = cell.getColumnCount(); int delta = 1; while ((c - delta) >= 0 && grid[r][c - delta] == cell) { delta++; } return cols - delta + 1; } return 0; } /** * Returns the number of rows in the table. */ public int getRowCount() { if (validateIfNecessary()) { return grid.length; } return 0; } /** * Returns the number of columns in the table. */ public int getColumnCount() { if (validateIfNecessary() && grid.length > 0) { return grid[0].length; } return 0; } // begin AccessibleTable implementation ... private AccessibleContext accessibleContext; public AccessibleContext getAccessibleContext() { if (accessibleContext == null) { accessibleContext = new TableAccessibleContext(this); } return accessibleContext; } /* * AccessibleContext for tables */ public class TableAccessibleContext extends HTMLAccessibleContext implements AccessibleTable { private AccessibleHeadersTable rowHeadersTable; public TableAccessibleContext(ElementInfo elementInfo) { super(elementInfo); } /** * Gets the accessibleName property of this object. The accessibleName * property of an object is a localized String that designates the purpose * of the object. For example, the accessibleName property of a label * or button might be the text of the label or button itself. In the * case of an object that doesn't display its name, the accessibleName * should still be set. For example, in the case of a text field used * to enter the name of a city, the accessibleName for the en_US locale * could be 'city.' * * @return the localized name of the object; null if this * object does not have a name * * @see #setAccessibleName */ public String getAccessibleName() { // return the role of the object return getAccessibleRole().toString(); } /** * Gets the accessibleDescription property of this object. If this * property isn't set, returns the content type of this * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text"). * * @return the localized description of the object; <code>null</code> * if this object does not have a description * * @see #setAccessibleName */ public String getAccessibleDescription() { return editor.getContentType(); } /** * Gets the role of this object. The role of the object is the generic * purpose or use of the class of this object. For example, the role * of a push button is AccessibleRole.PUSH_BUTTON. The roles in * AccessibleRole are provided so component developers can pick from * a set of predefined roles. This enables assistive technologies to * provide a consistent interface to various tweaked subclasses of * components (e.g., use AccessibleRole.PUSH_BUTTON for all components * that act like a push button) as well as distinguish between subclasses * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes * and AccessibleRole.RADIO_BUTTON for radio buttons). * <p>Note that the AccessibleRole class is also extensible, so * custom component developers can define their own AccessibleRole's * if the set of predefined roles is inadequate. * * @return an instance of AccessibleRole describing the role of the object * @see AccessibleRole */ public AccessibleRole getAccessibleRole() { return AccessibleRole.TABLE; } /** * Gets the 0-based index of this object in its accessible parent. * * @return the 0-based index of this object in its parent; -1 if this * object does not have an accessible parent. * * @see #getAccessibleParent * @see #getAccessibleChildrenCount * @gsee #getAccessibleChild */ public int getAccessibleIndexInParent() { return elementInfo.getIndexInParent(); } /** * Returns the number of accessible children of the object. * * @return the number of accessible children of the object. */ public int getAccessibleChildrenCount() { return ((TableElementInfo)elementInfo).getRowCount() * ((TableElementInfo)elementInfo).getColumnCount(); } /** * Returns the specified Accessible child of the object. The Accessible * children of an Accessible object are zero-based, so the first child * of an Accessible child is at index 0, the second child is at index 1, * and so on. * * @param i zero-based index of child * @return the Accessible child of the object * @see #getAccessibleChildrenCount */ public Accessible getAccessibleChild(int i) { int rowCount = ((TableElementInfo)elementInfo).getRowCount(); int columnCount = ((TableElementInfo)elementInfo).getColumnCount(); int r = i / rowCount; int c = i % columnCount; if (r < 0 || r >= rowCount || c < 0 || c >= columnCount) { return null; } else { return getAccessibleAt(r, c); } } public AccessibleTable getAccessibleTable() { return this; } /** * Returns the caption for the table. * * @return the caption for the table */ public Accessible getAccessibleCaption() { ElementInfo captionInfo = getCaptionInfo(); if (captionInfo instanceof Accessible) { return (Accessible)caption; } else { return null; } } /** * Sets the caption for the table. * * @param a the caption for the table */ public void setAccessibleCaption(Accessible a) { } /** * Returns the summary description of the table. * * @return the summary description of the table */ public Accessible getAccessibleSummary() { return null; } /** * Sets the summary description of the table * * @param a the summary description of the table */ public void setAccessibleSummary(Accessible a) { } /** * Returns the number of rows in the table. * * @return the number of rows in the table */ public int getAccessibleRowCount() { return ((TableElementInfo)elementInfo).getRowCount(); } /** * Returns the number of columns in the table. * * @return the number of columns in the table */ public int getAccessibleColumnCount() { return ((TableElementInfo)elementInfo).getColumnCount(); } /** * Returns the Accessible at a specified row and column * in the table. * * @param r zero-based row of the table * @param c zero-based column of the table * @return the Accessible at the specified row and column */ public Accessible getAccessibleAt(int r, int c) { TableCellElementInfo cellInfo = getCell(r, c); if (cellInfo != null) { return cellInfo.getAccessible(); } else { return null; } } /** * Returns the number of rows occupied by the Accessible at * a specified row and column in the table. * * @return the number of rows occupied by the Accessible at a * given specified (row, column) */ public int getAccessibleRowExtentAt(int r, int c) { return ((TableElementInfo)elementInfo).getRowExtentAt(r, c); } /** * Returns the number of columns occupied by the Accessible at * a specified row and column in the table. * * @return the number of columns occupied by the Accessible at a * given specified row and column */ public int getAccessibleColumnExtentAt(int r, int c) { return ((TableElementInfo)elementInfo).getColumnExtentAt(r, c); } /** * Returns the row headers as an AccessibleTable. * * @return an AccessibleTable representing the row * headers */ public AccessibleTable getAccessibleRowHeader() { return rowHeadersTable; } /** * Sets the row headers. * * @param table an AccessibleTable representing the * row headers */ public void setAccessibleRowHeader(AccessibleTable table) { } /** * Returns the column headers as an AccessibleTable. * * @return an AccessibleTable representing the column * headers */ public AccessibleTable getAccessibleColumnHeader() { return null; } /** * Sets the column headers. * * @param table an AccessibleTable representing the * column headers */ public void setAccessibleColumnHeader(AccessibleTable table) { } /** * Returns the description of the specified row in the table. * * @param r zero-based row of the table * @return the description of the row */ public Accessible getAccessibleRowDescription(int r) { return null; } /** * Sets the description text of the specified row of the table. * * @param r zero-based row of the table * @param a the description of the row */ public void setAccessibleRowDescription(int r, Accessible a) { } /** * Returns the description text of the specified column in the table. * * @param c zero-based column of the table * @return the text description of the column */ public Accessible getAccessibleColumnDescription(int c) { return null; } /** * Sets the description text of the specified column in the table. * * @param c zero-based column of the table * @param a the text description of the column */ public void setAccessibleColumnDescription(int c, Accessible a) { } /** * Returns a boolean value indicating whether the accessible at * a specified row and column is selected. * * @param r zero-based row of the table * @param c zero-based column of the table * @return the boolean value true if the accessible at the * row and column is selected. Otherwise, the boolean value * false */ public boolean isAccessibleSelected(int r, int c) { if (validateIfNecessary()) { if (r < 0 || r >= getAccessibleRowCount() || c < 0 || c >= getAccessibleColumnCount()) { return false; } TableCellElementInfo cell = getCell(r, c); if (cell != null) { Element elem = cell.getElement(); int start = elem.getStartOffset(); int end = elem.getEndOffset(); return start >= editor.getSelectionStart() && end <= editor.getSelectionEnd(); } } return false; } /** * Returns a boolean value indicating whether the specified row * is selected. * * @param r zero-based row of the table * @return the boolean value true if the specified row is selected. * Otherwise, false. */ public boolean isAccessibleRowSelected(int r) { if (validateIfNecessary()) { if (r < 0 || r >= getAccessibleRowCount()) { return false; } int nColumns = getAccessibleColumnCount(); TableCellElementInfo startCell = getCell(r, 0); if (startCell == null) { return false; } int start = startCell.getElement().getStartOffset(); TableCellElementInfo endCell = getCell(r, nColumns-1); if (endCell == null) { return false; } int end = endCell.getElement().getEndOffset(); return start >= editor.getSelectionStart() && end <= editor.getSelectionEnd(); } return false; } /** * Returns a boolean value indicating whether the specified column * is selected. * * @param c zero-based column of the table * @return the boolean value true if the specified column is selected. * Otherwise, false. */ public boolean isAccessibleColumnSelected(int c) { if (validateIfNecessary()) { if (c < 0 || c >= getAccessibleColumnCount()) { return false; } int nRows = getAccessibleRowCount(); TableCellElementInfo startCell = getCell(0, c); if (startCell == null) { return false; } int start = startCell.getElement().getStartOffset(); TableCellElementInfo endCell = getCell(nRows-1, c); if (endCell == null) { return false; } int end = endCell.getElement().getEndOffset(); return start >= editor.getSelectionStart() && end <= editor.getSelectionEnd(); } return false; } /** * Returns the selected rows in a table. * * @return an array of selected rows where each element is a * zero-based row of the table */ public int [] getSelectedAccessibleRows() { if (validateIfNecessary()) { int nRows = getAccessibleRowCount(); Vector<Integer> vec = new Vector<Integer>(); for (int i = 0; i < nRows; i++) { if (isAccessibleRowSelected(i)) { vec.addElement(Integer.valueOf(i)); } } int retval[] = new int[vec.size()]; for (int i = 0; i < retval.length; i++) { retval[i] = vec.elementAt(i).intValue(); } return retval; } return new int[0]; } /** * Returns the selected columns in a table. * * @return an array of selected columns where each element is a * zero-based column of the table */ public int [] getSelectedAccessibleColumns() { if (validateIfNecessary()) { int nColumns = getAccessibleRowCount(); Vector<Integer> vec = new Vector<Integer>(); for (int i = 0; i < nColumns; i++) { if (isAccessibleColumnSelected(i)) { vec.addElement(Integer.valueOf(i)); } } int retval[] = new int[vec.size()]; for (int i = 0; i < retval.length; i++) { retval[i] = vec.elementAt(i).intValue(); } return retval; } return new int[0]; } // begin AccessibleExtendedTable implementation ------------- /** * Returns the row number of an index in the table. * * @param index the zero-based index in the table * @return the zero-based row of the table if one exists; * otherwise -1. */ public int getAccessibleRow(int index) { if (validateIfNecessary()) { int numCells = getAccessibleColumnCount() * getAccessibleRowCount(); if (index >= numCells) { return -1; } else { return index / getAccessibleColumnCount(); } } return -1; } /** * Returns the column number of an index in the table. * * @param index the zero-based index in the table * @return the zero-based column of the table if one exists; * otherwise -1. */ public int getAccessibleColumn(int index) { if (validateIfNecessary()) { int numCells = getAccessibleColumnCount() * getAccessibleRowCount(); if (index >= numCells) { return -1; } else { return index % getAccessibleColumnCount(); } } return -1; } /** * Returns the index at a row and column in the table. * * @param r zero-based row of the table * @param c zero-based column of the table * @return the zero-based index in the table if one exists; * otherwise -1. */ public int getAccessibleIndex(int r, int c) { if (validateIfNecessary()) { if (r >= getAccessibleRowCount() || c >= getAccessibleColumnCount()) { return -1; } else { return r * getAccessibleColumnCount() + c; } } return -1; } /** * Returns the row header at a row in a table. * @param r zero-based row of the table * * @return a String representing the row header * if one exists; otherwise null. */ public String getAccessibleRowHeader(int r) { if (validateIfNecessary()) { TableCellElementInfo cellInfo = getCell(r, 0); if (cellInfo.isHeaderCell()) { View v = cellInfo.getView(); if (v != null && model != null) { try { return model.getText(v.getStartOffset(), v.getEndOffset() - v.getStartOffset()); } catch (BadLocationException e) { return null; } } } } return null; } /** * Returns the column header at a column in a table. * @param c zero-based column of the table * * @return a String representing the column header * if one exists; otherwise null. */ public String getAccessibleColumnHeader(int c) { if (validateIfNecessary()) { TableCellElementInfo cellInfo = getCell(0, c); if (cellInfo.isHeaderCell()) { View v = cellInfo.getView(); if (v != null && model != null) { try { return model.getText(v.getStartOffset(), v.getEndOffset() - v.getStartOffset()); } catch (BadLocationException e) { return null; } } } } return null; } public void addRowHeader(TableCellElementInfo cellInfo, int rowNumber) { if (rowHeadersTable == null) { rowHeadersTable = new AccessibleHeadersTable(); } rowHeadersTable.addHeader(cellInfo, rowNumber); } // end of AccessibleExtendedTable implementation ------------ protected class AccessibleHeadersTable implements AccessibleTable { // Header information is modeled as a Hashtable of // ArrayLists where each Hashtable entry represents // a row containing one or more headers. private Hashtable<Integer, ArrayList<TableCellElementInfo>> headers = new Hashtable<Integer, ArrayList<TableCellElementInfo>>(); private int rowCount = 0; private int columnCount = 0; public void addHeader(TableCellElementInfo cellInfo, int rowNumber) { Integer rowInteger = Integer.valueOf(rowNumber); ArrayList<TableCellElementInfo> list = headers.get(rowInteger); if (list == null) { list = new ArrayList<TableCellElementInfo>(); headers.put(rowInteger, list); } list.add(cellInfo); } /** * Returns the caption for the table. * * @return the caption for the table */ public Accessible getAccessibleCaption() { return null; } /** * Sets the caption for the table. * * @param a the caption for the table */ public void setAccessibleCaption(Accessible a) { } /** * Returns the summary description of the table. * * @return the summary description of the table */ public Accessible getAccessibleSummary() { return null; } /** * Sets the summary description of the table * * @param a the summary description of the table */ public void setAccessibleSummary(Accessible a) { } /** * Returns the number of rows in the table. * * @return the number of rows in the table */ public int getAccessibleRowCount() { return rowCount; } /** * Returns the number of columns in the table. * * @return the number of columns in the table */ public int getAccessibleColumnCount() { return columnCount; } private TableCellElementInfo getElementInfoAt(int r, int c) { ArrayList<TableCellElementInfo> list = headers.get(Integer.valueOf(r)); if (list != null) { return list.get(c); } else { return null; } } /** * Returns the Accessible at a specified row and column * in the table. * * @param r zero-based row of the table * @param c zero-based column of the table * @return the Accessible at the specified row and column */ public Accessible getAccessibleAt(int r, int c) { ElementInfo elementInfo = getElementInfoAt(r, c); if (elementInfo instanceof Accessible) { return (Accessible)elementInfo; } else { return null; } } /** * Returns the number of rows occupied by the Accessible at * a specified row and column in the table. * * @return the number of rows occupied by the Accessible at a * given specified (row, column) */ public int getAccessibleRowExtentAt(int r, int c) { TableCellElementInfo elementInfo = getElementInfoAt(r, c); if (elementInfo != null) { return elementInfo.getRowCount(); } else { return 0; } } /** * Returns the number of columns occupied by the Accessible at * a specified row and column in the table. * * @return the number of columns occupied by the Accessible at a * given specified row and column */ public int getAccessibleColumnExtentAt(int r, int c) { TableCellElementInfo elementInfo = getElementInfoAt(r, c); if (elementInfo != null) { return elementInfo.getRowCount(); } else { return 0; } } /** * Returns the row headers as an AccessibleTable. * * @return an AccessibleTable representing the row * headers */ public AccessibleTable getAccessibleRowHeader() { return null; } /** * Sets the row headers. * * @param table an AccessibleTable representing the * row headers */ public void setAccessibleRowHeader(AccessibleTable table) { } /** * Returns the column headers as an AccessibleTable. * * @return an AccessibleTable representing the column * headers */ public AccessibleTable getAccessibleColumnHeader() { return null; } /** * Sets the column headers. * * @param table an AccessibleTable representing the * column headers */ public void setAccessibleColumnHeader(AccessibleTable table) { } /** * Returns the description of the specified row in the table. * * @param r zero-based row of the table * @return the description of the row */ public Accessible getAccessibleRowDescription(int r) { return null; } /** * Sets the description text of the specified row of the table. * * @param r zero-based row of the table * @param a the description of the row */ public void setAccessibleRowDescription(int r, Accessible a) { } /** * Returns the description text of the specified column in the table. * * @param c zero-based column of the table * @return the text description of the column */ public Accessible getAccessibleColumnDescription(int c) { return null; } /** * Sets the description text of the specified column in the table. * * @param c zero-based column of the table * @param a the text description of the column */ public void setAccessibleColumnDescription(int c, Accessible a) { } /** * Returns a boolean value indicating whether the accessible at * a specified row and column is selected. * * @param r zero-based row of the table * @param c zero-based column of the table * @return the boolean value true if the accessible at the * row and column is selected. Otherwise, the boolean value * false */ public boolean isAccessibleSelected(int r, int c) { return false; } /** * Returns a boolean value indicating whether the specified row * is selected. * * @param r zero-based row of the table * @return the boolean value true if the specified row is selected. * Otherwise, false. */ public boolean isAccessibleRowSelected(int r) { return false; } /** * Returns a boolean value indicating whether the specified column * is selected. * * @param c zero-based column of the table * @return the boolean value true if the specified column is selected. * Otherwise, false. */ public boolean isAccessibleColumnSelected(int c) { return false; } /** * Returns the selected rows in a table. * * @return an array of selected rows where each element is a * zero-based row of the table */ public int [] getSelectedAccessibleRows() { return new int [0]; } /** * Returns the selected columns in a table. * * @return an array of selected columns where each element is a * zero-based column of the table */ public int [] getSelectedAccessibleColumns() { return new int [0]; } } } // ... end AccessibleHeadersTable /* * ElementInfo for table rows */ private class TableRowElementInfo extends ElementInfo { private TableElementInfo parent; private int rowNumber; TableRowElementInfo(Element e, TableElementInfo parent, int rowNumber) { super(e, parent); this.parent = parent; this.rowNumber = rowNumber; } protected void loadChildren(Element e) { for (int x = 0; x < e.getElementCount(); x++) { AttributeSet attrs = e.getElement(x).getAttributes(); if (attrs.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.TH) { TableCellElementInfo headerElementInfo = new TableCellElementInfo(e.getElement(x), this, true); addChild(headerElementInfo); AccessibleTable at = parent.getAccessibleContext().getAccessibleTable(); TableAccessibleContext tableElement = (TableAccessibleContext)at; tableElement.addRowHeader(headerElementInfo, rowNumber); } else if (attrs.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.TD) { addChild(new TableCellElementInfo(e.getElement(x), this, false)); } } } /** * Returns the max of the rowspans of the cells in this row. */ public int getRowCount() { int rowCount = 1; if (validateIfNecessary()) { for (int counter = 0; counter < getChildCount(); counter++) { TableCellElementInfo cell = (TableCellElementInfo) getChild(counter); if (cell.validateIfNecessary()) { rowCount = Math.max(rowCount, cell.getRowCount()); } } } return rowCount; } /** * Returns the sum of the column spans of the individual * cells in this row. */ public int getColumnCount() { int colCount = 0; if (validateIfNecessary()) { for (int counter = 0; counter < getChildCount(); counter++) { TableCellElementInfo cell = (TableCellElementInfo) getChild(counter); if (cell.validateIfNecessary()) { colCount += cell.getColumnCount(); } } } return colCount; } /** * Overriden to invalidate the table as well as * TableRowElementInfo. */ protected void invalidate(boolean first) { super.invalidate(first); getParent().invalidate(true); } /** * Places the TableCellElementInfos for this element in * the grid. */ private void updateGrid(int row) { if (validateIfNecessary()) { boolean emptyRow = false; while (!emptyRow) { for (int counter = 0; counter < grid[row].length; counter++) { if (grid[row][counter] == null) { emptyRow = true; break; } } if (!emptyRow) { row++; } } for (int col = 0, counter = 0; counter < getChildCount(); counter++) { TableCellElementInfo cell = (TableCellElementInfo) getChild(counter); while (grid[row][col] != null) { col++; } for (int rowCount = cell.getRowCount() - 1; rowCount >= 0; rowCount--) { for (int colCount = cell.getColumnCount() - 1; colCount >= 0; colCount--) { grid[row + rowCount][col + colCount] = cell; } } col += cell.getColumnCount(); } } } /** * Returns the column count of the number of columns that have * a rowcount >= rowspan. */ private int getColumnCount(int rowspan) { if (validateIfNecessary()) { int cols = 0; for (int counter = 0; counter < getChildCount(); counter++) { TableCellElementInfo cell = (TableCellElementInfo) getChild(counter); if (cell.getRowCount() >= rowspan) { cols += cell.getColumnCount(); } } return cols; } return 0; } } /** * TableCellElementInfo is used to represents the cells of * the table. */ private class TableCellElementInfo extends ElementInfo { private Accessible accessible; private boolean isHeaderCell; TableCellElementInfo(Element e, ElementInfo parent) { super(e, parent); this.isHeaderCell = false; } TableCellElementInfo(Element e, ElementInfo parent, boolean isHeaderCell) { super(e, parent); this.isHeaderCell = isHeaderCell; } /* * Returns whether this table cell is a header */ public boolean isHeaderCell() { return this.isHeaderCell; } /* * Returns the Accessible representing this table cell */ public Accessible getAccessible() { accessible = null; getAccessible(this); return accessible; } /* * Gets the outermost Accessible in the table cell */ private void getAccessible(ElementInfo elementInfo) { if (elementInfo instanceof Accessible) { accessible = (Accessible)elementInfo; } else { for (int i = 0; i < elementInfo.getChildCount(); i++) { getAccessible(elementInfo.getChild(i)); } } } /** * Returns the rowspan attribute. */ public int getRowCount() { if (validateIfNecessary()) { return Math.max(1, getIntAttr(getAttributes(), HTML.Attribute.ROWSPAN, 1)); } return 0; } /** * Returns the colspan attribute. */ public int getColumnCount() { if (validateIfNecessary()) { return Math.max(1, getIntAttr(getAttributes(), HTML.Attribute.COLSPAN, 1)); } return 0; } /** * Overriden to invalidate the TableRowElementInfo as well as * the TableCellElementInfo. */ protected void invalidate(boolean first) { super.invalidate(first); getParent().invalidate(true); } } } /** * ElementInfo provides a slim down view of an Element. Each ElementInfo * can have any number of child ElementInfos that are not necessarily * direct children of the Element. As the Document changes various * ElementInfos become invalidated. Before accessing a particular portion * of an ElementInfo you should make sure it is valid by invoking * <code>validateIfNecessary</code>, this will return true if * successful, on the other hand a false return value indicates the * ElementInfo is not valid and can never become valid again (usually * the result of the Element the ElementInfo encapsulates being removed). */ private class ElementInfo { /** * The children of this ElementInfo. */ private ArrayList<ElementInfo> children; /** * The Element this ElementInfo is providing information for. */ private Element element; /** * The parent ElementInfo, will be null for the root. */ private ElementInfo parent; /** * Indicates the validity of the ElementInfo. */ private boolean isValid; /** * Indicates if the ElementInfo can become valid. */ private boolean canBeValid; /** * Creates the root ElementInfo. */ ElementInfo(Element element) { this(element, null); } /** * Creates an ElementInfo representing <code>element</code> with * the specified parent. */ ElementInfo(Element element, ElementInfo parent) { this.element = element; this.parent = parent; isValid = false; canBeValid = true; } /** * Validates the receiver. This recreates the children as well. This * will be invoked within a <code>readLock</code>. If this is overriden * it MUST invoke supers implementation first! */ protected void validate() { isValid = true; loadChildren(getElement()); } /** * Recreates the direct children of <code>info</code>. */ protected void loadChildren(Element parent) { if (!parent.isLeaf()) { for (int counter = 0, maxCounter = parent.getElementCount(); counter < maxCounter; counter++) { Element e = parent.getElement(counter); ElementInfo childInfo = createElementInfo(e, this); if (childInfo != null) { addChild(childInfo); } else { loadChildren(e); } } } } /** * Returns the index of the child in the parent, or -1 for the * root or if the parent isn't valid. */ public int getIndexInParent() { if (parent == null || !parent.isValid()) { return -1; } return parent.indexOf(this); } /** * Returns the Element this <code>ElementInfo</code> represents. */ public Element getElement() { return element; } /** * Returns the parent of this Element, or null for the root. */ public ElementInfo getParent() { return parent; } /** * Returns the index of the specified child, or -1 if * <code>child</code> isn't a valid child. */ public int indexOf(ElementInfo child) { ArrayList<ElementInfo> children = this.children; if (children != null) { return children.indexOf(child); } return -1; } /** * Returns the child ElementInfo at <code>index</code>, or null * if <code>index</code> isn't a valid index. */ public ElementInfo getChild(int index) { if (validateIfNecessary()) { ArrayList<ElementInfo> children = this.children; if (children != null && index >= 0 && index < children.size()) { return children.get(index); } } return null; } /** * Returns the number of children the ElementInfo contains. */ public int getChildCount() { validateIfNecessary(); return (children == null) ? 0 : children.size(); } /** * Adds a new child to this ElementInfo. */ protected void addChild(ElementInfo child) { if (children == null) { children = new ArrayList<ElementInfo>(); } children.add(child); } /** * Returns the View corresponding to this ElementInfo, or null * if the ElementInfo can't be validated. */ protected View getView() { if (!validateIfNecessary()) { return null; } Object lock = lock(); try { View rootView = getRootView(); Element e = getElement(); int start = e.getStartOffset(); if (rootView != null) { return getView(rootView, e, start); } return null; } finally { unlock(lock); } } /** * Returns the Bounds for this ElementInfo, or null * if the ElementInfo can't be validated. */ public Rectangle getBounds() { if (!validateIfNecessary()) { return null; } Object lock = lock(); try { Rectangle bounds = getRootEditorRect(); View rootView = getRootView(); Element e = getElement(); if (bounds != null && rootView != null) { try { return rootView.modelToView(e.getStartOffset(), Position.Bias.Forward, e.getEndOffset(), Position.Bias.Backward, bounds).getBounds(); } catch (BadLocationException ble) { } } } finally { unlock(lock); } return null; } /** * Returns true if this ElementInfo is valid. */ protected boolean isValid() { return isValid; } /** * Returns the AttributeSet associated with the Element, this will * return null if the ElementInfo can't be validated. */ protected AttributeSet getAttributes() { if (validateIfNecessary()) { return getElement().getAttributes(); } return null; } /** * Returns the AttributeSet associated with the View that is * representing this Element, this will * return null if the ElementInfo can't be validated. */ protected AttributeSet getViewAttributes() { if (validateIfNecessary()) { View view = getView(); if (view != null) { return view.getElement().getAttributes(); } return getElement().getAttributes(); } return null; } /** * Convenience method for getting an integer attribute from the passed * in AttributeSet. */ protected int getIntAttr(AttributeSet attrs, Object key, int deflt) { if (attrs != null && attrs.isDefined(key)) { int i; String val = (String)attrs.getAttribute(key); if (val == null) { i = deflt; } else { try { i = Math.max(0, Integer.parseInt(val)); } catch (NumberFormatException x) { i = deflt; } } return i; } return deflt; } /** * Validates the ElementInfo if necessary. Some ElementInfos may * never be valid again. You should check <code>isValid</code> before * using one. This will reload the children and invoke * <code>validate</code> if the ElementInfo is invalid and can become * valid again. This will return true if the receiver is valid. */ protected boolean validateIfNecessary() { if (!isValid() && canBeValid) { children = null; Object lock = lock(); try { validate(); } finally { unlock(lock); } } return isValid(); } /** * Invalidates the ElementInfo. Subclasses should override this * if they need to reset state once invalid. */ protected void invalidate(boolean first) { if (!isValid()) { if (canBeValid && !first) { canBeValid = false; } return; } isValid = false; canBeValid = first; if (children != null) { for (ElementInfo child : children) { child.invalidate(false); } children = null; } } private View getView(View parent, Element e, int start) { if (parent.getElement() == e) { return parent; } int index = parent.getViewIndex(start, Position.Bias.Forward); if (index != -1 && index < parent.getViewCount()) { return getView(parent.getView(index), e, start); } return null; } private int getClosestInfoIndex(int index) { for (int counter = 0; counter < getChildCount(); counter++) { ElementInfo info = getChild(counter); if (index < info.getElement().getEndOffset() || index == info.getElement().getStartOffset()) { return counter; } } return -1; } private void update(DocumentEvent e) { if (!isValid()) { return; } ElementInfo parent = getParent(); Element element = getElement(); do { DocumentEvent.ElementChange ec = e.getChange(element); if (ec != null) { if (element == getElement()) { // One of our children changed. invalidate(true); } else if (parent != null) { parent.invalidate(parent == getRootInfo()); } return; } element = element.getParentElement(); } while (parent != null && element != null && element != parent.getElement()); if (getChildCount() > 0) { Element elem = getElement(); int pos = e.getOffset(); int index0 = getClosestInfoIndex(pos); if (index0 == -1 && e.getType() == DocumentEvent.EventType.REMOVE && pos >= elem.getEndOffset()) { // Event beyond our offsets. We may have represented this, // that is the remove may have removed one of our child // Elements that represented this, so, we should foward // to last element. index0 = getChildCount() - 1; } ElementInfo info = (index0 >= 0) ? getChild(index0) : null; if (info != null && (info.getElement().getStartOffset() == pos) && (pos > 0)) { // If at a boundary, forward the event to the previous // ElementInfo too. index0 = Math.max(index0 - 1, 0); } int index1; if (e.getType() != DocumentEvent.EventType.REMOVE) { index1 = getClosestInfoIndex(pos + e.getLength()); if (index1 < 0) { index1 = getChildCount() - 1; } } else { index1 = index0; // A remove may result in empty elements. while ((index1 + 1) < getChildCount() && getChild(index1 + 1).getElement().getEndOffset() == getChild(index1 + 1).getElement().getStartOffset()){ index1++; } } index0 = Math.max(index0, 0); // The check for isValid is here as in the process of // forwarding update our child may invalidate us. for (int i = index0; i <= index1 && isValid(); i++) { getChild(i).update(e); } } } } /** * DocumentListener installed on the current Document. Will invoke * <code>update</code> on the <code>RootInfo</code> in response to * any event. */ private class DocumentHandler implements DocumentListener { public void insertUpdate(DocumentEvent e) { getRootInfo().update(e); } public void removeUpdate(DocumentEvent e) { getRootInfo().update(e); } public void changedUpdate(DocumentEvent e) { getRootInfo().update(e); } } /* * PropertyChangeListener installed on the editor. */ private class PropertyChangeHandler implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("document")) { // handle the document change setDocument(editor.getDocument()); } } } }