Added Accessibility to FieldPanel

This commit is contained in:
ghidragon
2023-05-30 16:24:59 -04:00
parent 7dfaa2ccc3
commit 89e46f2ad9
18 changed files with 2163 additions and 34 deletions
@@ -0,0 +1,35 @@
/* ###
* IP: GHIDRA
*
* Licensed 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.util.viewer.field;
import docking.widgets.fieldpanel.FieldDescriptionProvider;
import docking.widgets.fieldpanel.field.Field;
import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.program.util.ProgramLocation;
public class ListingFieldDescriptionProvider implements FieldDescriptionProvider {
@Override
public String getDescription(FieldLocation loc, Field field) {
if (field instanceof ListingField listingField) {
FieldFactory fieldFactory = listingField.getFieldFactory();
ProgramLocation location = fieldFactory.getProgramLocation(0, 0, listingField);
return fieldFactory.getFieldName() + " Field at Address " + location.getAddress() +
" text = " + field.getText();
}
return "Unknown Field";
}
}
@@ -37,8 +37,7 @@ import ghidra.app.plugin.core.codebrowser.LayeredColorModel;
import ghidra.app.plugin.core.codebrowser.hover.ListingHoverService;
import ghidra.app.services.ButtonPressedListener;
import ghidra.app.util.ListingHighlightProvider;
import ghidra.app.util.viewer.field.FieldFactory;
import ghidra.app.util.viewer.field.ListingField;
import ghidra.app.util.viewer.field.*;
import ghidra.app.util.viewer.format.FieldHeader;
import ghidra.app.util.viewer.format.FormatManager;
import ghidra.app.util.viewer.util.*;
@@ -163,7 +162,9 @@ public class ListingPanel extends JPanel implements FieldMouseListener, FieldLoc
// extension point
protected FieldPanel createFieldPanel(LayoutModel model) {
return new FieldPanel(model);
FieldPanel fp = new FieldPanel(model, "Listing");
fp.setFieldDescriptionProvider(new ListingFieldDescriptionProvider());
return fp;
}
// extension point
@@ -887,7 +888,7 @@ public class ListingPanel extends JPanel implements FieldMouseListener, FieldLoc
ListingField field = (ListingField) fieldPanel.getFieldAt(point.x, point.y, dropLoc);
if (field != null) {
return field.getFieldFactory()
.getProgramLocation(dropLoc.getRow(), dropLoc.getCol(), field);
.getProgramLocation(dropLoc.getRow(), dropLoc.getCol(), field);
}
return null;
}
@@ -1157,7 +1158,8 @@ public class ListingPanel extends JPanel implements FieldMouseListener, FieldLoc
}
public void setFormatManager(FormatManager formatManager) {
List<ListingHighlightProvider> highlightProviders = this.formatManager.getHighlightProviders();
List<ListingHighlightProvider> highlightProviders =
this.formatManager.getHighlightProviders();
this.formatManager = formatManager;
@@ -392,7 +392,7 @@ public class OptionsGui extends JPanel {
* builds the preview panel.
*/
private JComponent buildPreviewPanel() {
fieldPanel = new FieldPanel(new SimpleLayoutModel());
fieldPanel = new FieldPanel(new SimpleLayoutModel(), "Preview");
IndexedScrollPane scroll = new IndexedScrollPane(fieldPanel);
return scroll;
}
@@ -79,7 +79,8 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene
*/
protected ByteViewerComponent(ByteViewerPanel vpanel, ByteViewerLayoutModel layoutModel,
DataFormatModel model, int bytesPerLine, FontMetrics fm) {
super(layoutModel);
super(layoutModel, "Byte Viewer");
setFieldDescriptionProvider((l, f) -> getFieldDescription(l, f));
this.panel = vpanel;
this.model = model;
@@ -94,6 +95,17 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene
setBackgroundColorModel(new ByteViewerBackgroundColorModel());
}
private String getFieldDescription(FieldLocation fieldLoc, Field field) {
ByteBlockInfo info = indexMap.getBlockInfo(fieldLoc.getIndex(), fieldLoc.getFieldNum());
if (info != null) {
String modelName = model.getName();
return modelName + " format at " +
info.getBlock().getLocationRepresentation(info.getOffset()) + ", value = " +
field.getText();
}
return null;
}
@Override
public void buttonPressed(FieldLocation fieldLocation, Field field, MouseEvent mouseEvent) {
if (fieldLocation == null || field == null) {
@@ -468,8 +480,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene
else {
++endFieldOffset;
}
fsel.addRange(
new FieldLocation(startLoc.getIndex(), startLoc.getFieldNum(), 0, 0),
fsel.addRange(new FieldLocation(startLoc.getIndex(), startLoc.getFieldNum(), 0, 0),
new FieldLocation(endIndex, endFieldOffset, 0, 0));
}
return fsel;
@@ -752,7 +752,7 @@ public class ByteViewerPanel extends JPanel
// for the index/address column
indexFactory = new IndexFieldFactory(fm);
indexPanel = new FieldPanel(this);
indexPanel = new FieldPanel(this, "Byte Viewer");
indexPanel.enableSelection(false);
indexPanel.setCursorOn(false);
@@ -1297,7 +1297,11 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
private class DecompilerFieldPanel extends FieldPanel {
public DecompilerFieldPanel(LayoutModel model) {
super(model);
super(model, "Decompiler");
// In the decompiler each field represents a line, so make the field description
// simply be the line number
setFieldDescriptionProvider(
(l, f) -> "line " + (l.getIndex().intValue() + 1) + ", " + f.getText());
}
/**
@@ -25,6 +25,7 @@ import docking.widgets.fieldpanel.*;
import ghidra.app.plugin.core.functiongraph.FGColorProvider;
import ghidra.app.plugin.core.functiongraph.mvc.FGController;
import ghidra.app.plugin.core.functiongraph.mvc.FunctionGraphOptions;
import ghidra.app.util.viewer.field.ListingFieldDescriptionProvider;
import ghidra.app.util.viewer.format.FormatManager;
import ghidra.app.util.viewer.listingpanel.*;
import ghidra.program.model.address.AddressSetView;
@@ -142,7 +143,8 @@ public class FGVertexListingPanel extends ListingPanel {
private class FGVertexFieldPanel extends FieldPanel {
public FGVertexFieldPanel(LayoutModel model) {
super(model);
super(model, "Function Graph Listing Vertex");
setFieldDescriptionProvider(new ListingFieldDescriptionProvider());
}
@Override
@@ -43,7 +43,7 @@ public class ActionAdapter implements Action, PropertyChangeListener {
* <p>Most clients should use {@link #ActionAdapter(DockingActionIf, ActionContextProvider)}
* @param dockingAction the action to adapt
*/
ActionAdapter(DockingActionIf dockingAction) {
public ActionAdapter(DockingActionIf dockingAction) {
this(dockingAction, null);
}
@@ -0,0 +1,494 @@
/* ###
* IP: GHIDRA
*
* Licensed 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.fieldpanel;
import java.awt.*;
import java.awt.event.FocusListener;
import java.text.BreakIterator;
import java.util.Locale;
import javax.accessibility.*;
import javax.swing.JComponent;
import javax.swing.text.AttributeSet;
import docking.widgets.fieldpanel.field.Field;
import docking.widgets.fieldpanel.support.RowColLocation;
/**
* Implements Accessible interfaces for individual fields in the field panel
*/
public class AccessibleField extends AccessibleContext
implements Accessible, AccessibleComponent, AccessibleText {
private Field field;
private int indexInParent;
private Rectangle boundsInParent;
private Locale locale;
private JComponent parent;
private int caretPos = 0;
private boolean isSelected = false;
/**
* Constructor
* @param field the field this is providing accessible access to
* @param parent the component containing the field (FieldPanel)
* @param indexInParent the number of this field relative to the visible fields on the screen.
* @param bounds the bounds of the field relative to the field panel.
*/
public AccessibleField(Field field, JComponent parent, int indexInParent, Rectangle bounds) {
this.field = field;
this.parent = parent;
this.indexInParent = indexInParent;
this.locale = parent.getLocale();
this.boundsInParent = bounds;
setAccessibleName("Field");
}
/**
* Sets the position of the cursor relative to the text in this field. It is only meaningful
* when the corresponding field is the field containing the field panel's actual cursor.
* @param caretPos the offset into the text of the field of where the cursor is being displayed
* by the field panel.
*/
public void setCaretPos(int caretPos) {
if (caretPos >= 0 && caretPos < field.getText().length()) {
this.caretPos = caretPos;
}
}
/**
* Sets that this field is part of the overall selection.
* @param selected true if the field is part of the selection; false otherwise
*/
public void setSelected(boolean selected) {
this.isSelected = selected;
}
/**
* Returns true if the field is currently part of a selection.
* @return true if the field is currently part of a selection.
*/
public boolean isSelected() {
return isSelected;
}
/**
* Returns the text of the field
* @return the text of the field
*/
public String getText() {
return field.getText();
}
/**
* Converts a row,col position to an text offset in the field
* @param row the row
* @param col the col
* @return an offset into the text that represents the row,col position
*/
public int getTextOffset(int row, int col) {
return field.screenLocationToTextOffset(row, col);
}
/**
* Returns the field associated with this AccessibleField.
* @return the field associated with this AccessibleField
*/
public Field getField() {
return field;
}
//==================================================================================================
// Accessible methods
//==================================================================================================
@Override
public AccessibleContext getAccessibleContext() {
return this;
}
//==================================================================================================
// AccessibleContext methods
//==================================================================================================
@Override
public AccessibleText getAccessibleText() {
return this;
}
@Override
public AccessibleComponent getAccessibleComponent() {
return this;
}
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.TEXT;
}
@Override
public AccessibleStateSet getAccessibleStateSet() {
AccessibleStateSet states = new AccessibleStateSet();
states.add(AccessibleState.MULTI_LINE);
states.add(AccessibleState.TRANSIENT);
return states;
}
@Override
public int getAccessibleIndexInParent() {
return indexInParent;
}
@Override
public int getAccessibleChildrenCount() {
return 0;
}
@Override
public Accessible getAccessibleChild(int i) {
return null;
}
@Override
public Locale getLocale() throws IllegalComponentStateException {
return locale;
}
//==================================================================================================
// AccessibleText methods
//==================================================================================================
@Override
public int getIndexAtPoint(Point p) {
// fields are weird, internally their 0 y position is the font baseline, so we
// need to compensate for that to find the row. Also, fields internal x position
// is relative to the field panel and the p being given here is relative to the field,
// we need to add the fields startingX to the given point.
int row = field.getRow(p.y - field.getHeightAbove());
int col = field.getCol(row, p.x + field.getStartX());
int result = field.screenLocationToTextOffset(row, col);
return result;
}
@Override
public Rectangle getCharacterBounds(int i) {
if (i < 0 || i >= getCharCount()) {
return new Rectangle(0, 0, 0, 0);
}
RowColLocation rowCol = field.textOffsetToScreenLocation(i);
int row = rowCol.row();
int col = rowCol.col();
Rectangle charBounds = field.getCursorBounds(row, col);
Rectangle nextCharBounds = field.getCursorBounds(row, col + 1);
charBounds.width = nextCharBounds.x - charBounds.x;
// again the bounds give are relative to the layout and field panel and this method wants
// a bounds relative to the field.
charBounds.y += field.getHeightAbove();
charBounds.x -= field.getStartX();
return charBounds;
}
@Override
public int getCharCount() {
return field.getText().length();
}
@Override
public String getAtIndex(int part, int index) {
String text = field.getText();
if (index < 0 || index >= text.length()) {
return null;
}
switch (part) {
case AccessibleText.CHARACTER:
return text.substring(index, index + 1);
case AccessibleText.WORD:
BreakIterator words = BreakIterator.getWordInstance(locale);
words.setText(text);
int end = words.following(index);
return text.substring(words.previous(), end);
case AccessibleText.SENTENCE:
BreakIterator sentences = BreakIterator.getSentenceInstance(locale);
sentences.setText(text);
end = sentences.following(index);
return text.substring(sentences.previous(), end);
default:
return null;
}
}
@Override
public String getAfterIndex(int part, int index) {
String text = field.getText();
if (index < 0 || index >= text.length() - 1) {
return null;
}
switch (part) {
case AccessibleText.CHARACTER:
return text.substring(index + 1, index + 2);
case AccessibleText.WORD:
BreakIterator words = BreakIterator.getWordInstance(locale);
words.setText(text);
int start = words.following(index);
if (start == BreakIterator.DONE || start >= text.length()) {
return null;
}
int end = words.following(start);
if (end == BreakIterator.DONE || end > text.length()) {
return null;
}
return text.substring(start, end);
case AccessibleText.SENTENCE:
BreakIterator sentences = BreakIterator.getSentenceInstance(locale);
sentences.setText(text);
start = sentences.following(index);
if (start == BreakIterator.DONE || start > text.length()) {
return null;
}
end = sentences.following(start);
if (end == BreakIterator.DONE || end > text.length()) {
return null;
}
return text.substring(start, end);
default:
return null;
}
}
@Override
public String getBeforeIndex(int part, int index) {
String text = field.getText();
if (index < 1 || index > text.length()) {
return null;
}
switch (part) {
case AccessibleText.CHARACTER:
return text.substring(index - 1, index);
case AccessibleText.WORD:
BreakIterator words = BreakIterator.getWordInstance(locale);
words.setText(text);
// move to the beginning of the current word so the algorithm
// gives us the previous word and not the word we are on. Note: this is needed
// because the preceding() method behaves differently if in the middle of a
// word than if at the beginning of the word.
if (!words.isBoundary(index)) {
words.preceding(index);
}
int start = words.previous();
int end = words.next();
if (start == BreakIterator.DONE) {
return null;
}
return text.substring(start, end);
case AccessibleText.SENTENCE:
BreakIterator sentences = BreakIterator.getSentenceInstance(locale);
sentences.setText(text);
if (!sentences.isBoundary(index)) {
sentences.preceding(index);
}
start = sentences.previous();
end = sentences.next();
if (start == BreakIterator.DONE) {
return null;
}
return text.substring(start, end);
default:
return null;
}
}
@Override
public int getCaretPosition() {
return caretPos;
}
@Override
public AttributeSet getCharacterAttribute(int i) {
return null;
}
@Override
public int getSelectionStart() {
// field selection is all or nothing so this always returns 0
return 0;
}
@Override
public int getSelectionEnd() {
// field selection is all or nothing, so if selected this will return the end of the text
// otherwise, return 0 because if selectionStart == selectionEnd means no selection
if (isSelected) {
return field.getText().length();
}
return 0;
}
@Override
public String getSelectedText() {
// selection is all or nothing
if (isSelected) {
return field.getText();
}
return null;
}
//==================================================================================================
// AccessibleComponent methods
//==================================================================================================
@Override
public Color getBackground() {
return parent.getBackground();
}
@Override
public void setBackground(Color c) {
// unsupported
}
@Override
public Color getForeground() {
return parent.getForeground();
}
@Override
public void setForeground(Color c) {
// unsupported
}
@Override
public Cursor getCursor() {
return parent.getCursor();
}
@Override
public void setCursor(Cursor cursor) {
// unsupported
}
@Override
public Font getFont() {
return parent.getFont();
}
@Override
public void setFont(Font f) {
// unsupported
}
@Override
public FontMetrics getFontMetrics(Font f) {
return parent.getFontMetrics(f);
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void setEnabled(boolean b) {
// unsupported
}
@Override
public boolean isVisible() {
return true;
}
@Override
public void setVisible(boolean b) {
// unsupported
}
@Override
public boolean isShowing() {
return true;
}
@Override
public boolean contains(Point p) {
return (p.x >= 0) && (p.x < field.getWidth()) && (p.y >= 0) && (p.y < field.getHeight());
}
@Override
public Point getLocationOnScreen() {
Point parentLoc = parent.getLocationOnScreen();
return new Point(parentLoc.x + boundsInParent.x, parentLoc.y + boundsInParent.y);
}
@Override
public Point getLocation() {
return boundsInParent.getLocation();
}
@Override
public void setLocation(Point p) {
// unsupported
}
@Override
public Rectangle getBounds() {
return new Rectangle(boundsInParent);
}
@Override
public void setBounds(Rectangle r) {
// unsupported
}
@Override
public Dimension getSize() {
return new Dimension(field.getWidth(), field.getHeight());
}
@Override
public void setSize(Dimension d) {
// unsupported
}
@Override
public Accessible getAccessibleAt(Point p) {
return null;
}
@Override
public boolean isFocusTraversable() {
return false;
}
@Override
public void requestFocus() {
// unsupported
}
@Override
public void addFocusListener(FocusListener l) {
// unsupported
}
@Override
public void removeFocusListener(FocusListener l) {
// unsupported
}
}
@@ -0,0 +1,453 @@
/* ###
* IP: GHIDRA
*
* Licensed 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.fieldpanel;
import static javax.accessibility.AccessibleContext.*;
import java.awt.Point;
import java.awt.Rectangle;
import java.math.BigInteger;
import java.util.*;
import javax.accessibility.*;
import javax.swing.JComponent;
import docking.widgets.EventTrigger;
import docking.widgets.fieldpanel.field.Field;
import docking.widgets.fieldpanel.support.*;
/**
* Contains all the code for implementing the AccessibleFieldPanel which is an inner class in
* the FieldPanel class. The AccessibleFieldPanel has to be declared as an inner class because
* it needs to extends AccessibleJComponent which is a non-static inner class of JComponent.
* However, we did not want to put all the logic in there as FieldPanel is already an
* extremely large and complex class. Also, by delegating the the logic, testing is much
* easier.
* <P>
* The model for accessibility for the FieldPanel is a bit complex because
* the field panel displays text, but in a 2 dimensional array of fields, where each field
* has potentially 2 dimensional text. So for the purpose of accessibility, the FieldPanel
* acts as both a text field and a text component.
* <P>
* To support screen readers reacting to cursor movements in the FieldPanel, the FieldPanel
* acts like a text field, but it acts like it only has the text of one inner Field at a time
* (The one where the cursor is). The other approach that was considered was to treat the field
* panel as a single text document. This would be difficult to implement because of the way fields
* are multi-lined. Also, the user of the screen reader would lose all concepts that there are
* fields. By maintaining the fields as a concept to the screen reader, it can provide more
* meaningful descriptions as the cursor is moved between fields.
* <P>
* The Field panel also acts as an {@link AccessibleComponent} with virtual children for each of its
* visible fields. This is what allows screen readers to read the context of whatever the mouse
* is hovering over keeping the data separated by the field boundaries.
*/
public class AccessibleFieldPanelDelegate {
private List<AccessibleLayout> accessibleLayouts;
private int totalFieldCount;
private AccessibleField[] fieldsCache;
private JComponent panel;
// caret position tracking
private FieldLocation cursorLoc;
private int caretPos;
private AccessibleField cursorField;
private FieldDescriptionProvider fieldDescriber = (l, f) -> "";
private AccessibleContext context;
private String description;
private FieldSelection currentSelection;
public AccessibleFieldPanelDelegate(List<AnchoredLayout> layouts, AccessibleContext context,
JComponent panel) {
this.context = context;
this.panel = panel;
setLayouts(layouts);
}
/**
* Whenever the set of visible layouts changes, the field panel rebuilds its info for the
* new visible fields and notifies the accessibility system that its children changed.
* @param layouts the new set of visible layouts.
*/
public void setLayouts(List<AnchoredLayout> layouts) {
totalFieldCount = 0;
accessibleLayouts = new ArrayList<>(layouts.size());
for (AnchoredLayout layout : layouts) {
AccessibleLayout accessibleLayout = new AccessibleLayout(layout, totalFieldCount);
accessibleLayouts.add(accessibleLayout);
totalFieldCount += layout.getNumFields();
}
fieldsCache = new AccessibleField[totalFieldCount];
context.firePropertyChange(ACCESSIBLE_INVALIDATE_CHILDREN, null, panel);
}
/**
* Tells this delegate that the cursor moved. It updates its internal state and fires
* events to the accessibility system.
* @param newCursorLoc the new FieldLoation of the cursor
* @param trigger the event trigger
*/
public void setCaret(FieldLocation newCursorLoc, EventTrigger trigger) {
if (cursorField == null || !isSameField(cursorLoc, newCursorLoc)) {
AccessibleTextSequence oldSequence = getAccessibleTextSequence(cursorField);
cursorLoc = newCursorLoc;
cursorField = getAccessibleField(newCursorLoc);
AccessibleTextSequence newSequence = getAccessibleTextSequence(cursorField);
String oldDescription = description;
description = generateDescription();
if (trigger == EventTrigger.GUI_ACTION) {
context.firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, oldSequence, newSequence);
context.firePropertyChange(ACCESSIBLE_DESCRIPTION_PROPERTY, oldDescription,
description);
}
if (currentSelection != null && currentSelection.contains(cursorLoc)) {
updateCurrentFieldSelectedState(trigger);
}
caretPos = -1;
}
if (cursorField == null) {
caretPos = 0;
return;
}
int newCaretPos = cursorField.getTextOffset(newCursorLoc.getRow(), newCursorLoc.getCol());
cursorField.setCaretPos(newCaretPos);
if (newCaretPos != caretPos && trigger == EventTrigger.GUI_ACTION) {
context.firePropertyChange(ACCESSIBLE_CARET_PROPERTY, caretPos, newCaretPos);
}
caretPos = newCaretPos;
cursorLoc = newCursorLoc;
}
/**
* Tells this delegate that the selection has changed. If the current field is in the selection,
* it sets the current AccessibleField to be selected. (A field is either entirely selected
* or not)
* @param currentSelection the new current field panel selection
* @param trigger the event trigger
*/
public void setSelection(FieldSelection currentSelection, EventTrigger trigger) {
this.currentSelection = currentSelection;
updateCurrentFieldSelectedState(trigger);
}
private void updateCurrentFieldSelectedState(EventTrigger trigger) {
if (cursorField == null) {
return;
}
boolean oldIsSelected = cursorField.isSelected();
boolean newIsSelected = currentSelection != null && currentSelection.contains(cursorLoc);
cursorField.setSelected(newIsSelected);
if (oldIsSelected != newIsSelected && trigger == EventTrigger.GUI_ACTION) {
context.firePropertyChange(ACCESSIBLE_SELECTION_PROPERTY, null, null);
}
}
private String generateDescription() {
Field field = cursorField != null ? cursorField.getField() : null;
return fieldDescriber.getDescription(cursorLoc, field);
}
private AccessibleTextSequence getAccessibleTextSequence(AccessibleField field) {
if (field == null) {
return new AccessibleTextSequence(0, 0, "");
}
String text = field.getField().getText();
return new AccessibleTextSequence(0, text.length(), text);
}
/**
* Returns the caret position relative the current active field.
* @return the caret position relative the current active field
*/
public int getCaretPosition() {
return caretPos;
}
/**
* Returns the number of characters in the current active field.
* @return the number of characters in the current active field.
*/
public int getCharCount() {
return cursorField != null ? cursorField.getCharCount() : 0;
}
private boolean isSameField(FieldLocation loc1, FieldLocation loc2) {
if (loc1.getIndex() != loc2.getIndex()) {
return false;
}
return loc1.getFieldNum() == loc2.getFieldNum();
}
/**
* Returns the n'th AccessibleField that is visible on the screen.
* @param fieldNum the number of the field to get
* @return the n'th AccessibleField that is visible on the screen
*/
public AccessibleField getAccessibleField(int fieldNum) {
if (fieldNum < 0 || fieldNum >= fieldsCache.length) {
return null;
}
if (fieldsCache[fieldNum] == null) {
fieldsCache[fieldNum] = createAccessibleField(fieldNum);
}
return fieldsCache[fieldNum];
}
/**
* Returns the AccessibleField associated with the given field location.
* @param loc the FieldLocation to get the visible field for
* @return the AccessibleField associated with the given field location
*/
public AccessibleField getAccessibleField(FieldLocation loc) {
int result = Collections.binarySearch(accessibleLayouts, loc.getIndex(),
Comparator.comparing(
o -> o instanceof AccessibleLayout lh ? lh.getIndex() : (BigInteger) o,
BigInteger::compareTo));
if (result < 0) {
return null;
}
AccessibleLayout layout = accessibleLayouts.get(result);
return getAccessibleField(layout.getStartingFieldNum() + loc.getFieldNum());
}
private AccessibleField createAccessibleField(int fieldNum) {
int result = Collections.binarySearch(accessibleLayouts, fieldNum, Comparator.comparingInt(
o -> o instanceof AccessibleLayout lh ? lh.getStartingFieldNum() : (Integer) o));
if (result < 0) {
result = -result - 2;
}
AccessibleLayout layout = accessibleLayouts.get(result);
return layout.createAccessibleField(fieldNum);
}
/**
* Return the bounds relative to the field panel for the character at the given index
* @param index the index of the character in the active field whose bounds is to be returned.
* @return the bounds relative to the field panel for the character at the given index
*/
public Rectangle getCharacterBounds(int index) {
if (cursorField == null) {
return null;
}
Point loc = cursorField.getLocation();
Rectangle bounds = cursorField.getCharacterBounds(index);
bounds.x += loc.x;
bounds.y += loc.y;
return bounds;
}
/**
* Returns the character index at the given point relative to the FieldPanel. Note this
* only returns chars in the active field.
* @param p the point to get the character for
* @return the character index at the given point relative to the FieldPanel.
*/
public int getIndexAtPoint(Point p) {
if (cursorField == null) {
return 0;
}
Rectangle bounds = cursorField.getBounds();
if (!bounds.contains(p)) {
return -1;
}
Point localPoint = new Point(p.x - bounds.x, p.y - bounds.y);
return cursorField.getIndexAtPoint(localPoint);
}
/**
* Returns the char, word, or sentence at the given char index.
* @param part specifies char, word or sentence (See {@link AccessibleText})
* @param index the character index to get data for
* @return the char, word, or sentences at the given char index
*/
public String getAtIndex(int part, int index) {
if (cursorField == null) {
return "";
}
return cursorField.getAtIndex(part, index);
}
/**
* Returns the char, word, or sentence after the given char index.
* @param part specifies char, word or sentence (See {@link AccessibleText})
* @param index the character index to get data for
* @return the char, word, or sentence after the given char index
*/
public String getAfterIndex(int part, int index) {
if (cursorField == null) {
return "";
}
return cursorField.getAfterIndex(part, index);
}
/**
* Returns the char, word, or sentence at the given char index.
* @param part specifies char, word or sentence (See {@link AccessibleText})
* @param index the character index to get data for
* @return the char, word, or sentence at the given char index
*/
public String getBeforeIndex(int part, int index) {
if (cursorField == null) {
return "";
}
return cursorField.getBeforeIndex(part, index);
}
/**
* Returns the number of visible field showing on the screen in the field panel.
* @return the number of visible field showing on the screen in the field panel
*/
public int getFieldCount() {
return totalFieldCount;
}
/**
* Returns the {@link AccessibleField} that is at the given point relative to the FieldPanel.
* @param p the point to get an Accessble child at
* @return the {@link AccessibleField} that is at the given point relative to the FieldPanel
*/
public Accessible getAccessibleAt(Point p) {
int result = Collections.binarySearch(accessibleLayouts, p.y, Comparator
.comparingInt(o -> o instanceof AccessibleLayout lh ? lh.getYpos() : (Integer) o));
if (result < 0) {
result = -result - 2;
}
if (result < 0 || result >= accessibleLayouts.size()) {
return null;
}
int fieldNum = accessibleLayouts.get(result).getFieldNum(p);
return getAccessibleField(fieldNum);
}
/**
* Returns a description of the current field
* @return a description of the current field
*/
public String getFieldDescription() {
return description;
}
/**
* Sets the {@link FieldDescriptionProvider} that can generate descriptions of the current
* field.
* @param provider the description provider
*/
public void setFieldDescriptionProvider(FieldDescriptionProvider provider) {
fieldDescriber = provider;
}
/**
* Returns the selection character start index. This currently always returns 0 as
* selections are all or nothing.
* @return the selection character start index.
*/
public int getSelectionStart() {
if (cursorField == null) {
return 0;
}
return cursorField.getSelectionStart();
}
/**
* Returns the selection character end index. This is either 0, indicating there is no selection
* or the index at the end of the text meaning the entire field is selected.
* @return the selection character start index.
*/
public int getSelectionEnd() {
if (cursorField == null) {
return 0;
}
return cursorField.getSelectionEnd();
}
/**
* Returns either null if the field is not selected or the full field text if it is selected.
* @return either null if the field is not selected or the full field text if it is selected
*/
public String getSelectedText() {
if (cursorField == null) {
return null;
}
return cursorField.getSelectedText();
}
/**
* Wraps each AnchoredLayout to assist organizing the list of layouts into a single list
* of fields.
*/
private class AccessibleLayout {
private AnchoredLayout layout;
private int startingFieldNum;
public AccessibleLayout(AnchoredLayout layout, int startingFieldNum) {
this.layout = layout;
this.startingFieldNum = startingFieldNum;
}
/**
* Creates the AccessibleField as needed.
* @param fieldNum the number of the field to create an AccessibleField for. This number
* is relative to all the fields in the field panel and not to this layout.
* @return an AccessibleField for the given fieldNum
*/
public AccessibleField createAccessibleField(int fieldNum) {
int fieldNumInLayout = fieldNum - startingFieldNum;
Field field = layout.getField(fieldNumInLayout);
Rectangle fieldBounds = layout.getFieldBounds(fieldNumInLayout);
return new AccessibleField(field, panel, fieldNum, fieldBounds);
}
/**
* Returns the overall field number of the first field in this layout. For example,
* the first layout would have a starting field number of 0 and if it has 5 fields, the
* next layout would have a starting field number of 5 and so on.
* @return the overall field number of the first field in this layout.
*/
public int getStartingFieldNum() {
return startingFieldNum;
}
/**
* Returns the overall field number of the field containing the given point.
* @param p the point to find the field for
* @return the overall field number of the field containing the given point.
*/
public int getFieldNum(Point p) {
return layout.getFieldIndex(p.x, p.y) + startingFieldNum;
}
/**
* Return the y position of this layout relative to the field panel.
* @return the y position of this layout relative to the field panel.
*/
public int getYpos() {
return layout.getYPos();
}
/**
* Returns the index of the layout as defined by the client code. The only requirements for
* indexes is that the index for a layout is always bigger then the index of the previous
* layout.
* @return the index of the layout as defined by the client code.
*/
public BigInteger getIndex() {
return layout.getIndex();
}
}
}
@@ -0,0 +1,33 @@
/* ###
* IP: GHIDRA
*
* Licensed 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.fieldpanel;
import docking.widgets.fieldpanel.field.Field;
import docking.widgets.fieldpanel.support.FieldLocation;
/**
* Provides descriptions for fields in a field panel
*/
public interface FieldDescriptionProvider {
/**
* Gets a description for the given location and field.
* @param loc the FieldLocation to get a description for
* @param field the Field to get a description for
* @return a String describing the given field location
*/
public String getDescription(FieldLocation loc, Field field);
}
@@ -23,10 +23,12 @@ import java.math.BigInteger;
import java.util.*;
import java.util.List;
import javax.accessibility.*;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.AttributeSet;
import docking.DockingUtils;
import docking.util.GraphicsUtils;
@@ -43,7 +45,7 @@ import generic.theme.GThemeDefaults.Colors.Messages;
import ghidra.util.*;
public class FieldPanel extends JPanel
implements IndexedScrollable, LayoutModelListener, ChangeListener {
implements IndexedScrollable, LayoutModelListener, ChangeListener, Accessible {
public static final int MOUSEWHEEL_LINES_TO_SCROLL = 3;
private LayoutModel model;
@@ -81,13 +83,30 @@ public class FieldPanel extends JPanel
private int currentViewXpos;
private JViewport viewport;
private String name;
private FieldDescriptionProvider fieldDescriptionProvider;
private AccessibleFieldPanel accessibleFieldPanel;
public FieldPanel(LayoutModel model) {
this(model, "No Name");
}
public FieldPanel(LayoutModel model, String name) {
this.model = model;
this.name = name;
model.addLayoutModelListener(this);
layoutHandler = new AnchoredLayoutHandler(model, getHeight());
layouts = layoutHandler.positionLayoutsAroundAnchor(BigInteger.ZERO, 0);
// initialize the focus traversal keys to control Tab to free up the tab key for internal
// field panel use. This is the same behavior that text components use.
KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.CTRL_DOWN_MASK);
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Set.of(ks));
ks = KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Set.of(ks));
addKeyListener(new FieldPanelKeyAdapter());
addMouseListener(new FieldPanelMouseAdapter());
addMouseMotionListener(new FieldPanelMouseMotionAdapter());
@@ -130,6 +149,13 @@ public class FieldPanel extends JPanel
repaint();
}
public void setFieldDescriptionProvider(FieldDescriptionProvider provider) {
fieldDescriptionProvider = provider;
if (accessibleFieldPanel != null) {
accessibleFieldPanel.setFieldDescriptionProvider(provider);
}
}
/**
* Makes sure the location is completely visible on the screen. If it already is visible, this
* routine will do nothing. If the location is above the screen (at an index less than the first
@@ -272,6 +298,14 @@ public class FieldPanel extends JPanel
cursorHandler.doCursorRight(EventTrigger.API_CALL);
}
public void tabRight() {
cursorHandler.doTabRight(API_CALL);
}
public void tabLeft() {
cursorHandler.doTabLeft(API_CALL);
}
/**
* Moves the cursor to the beginning of the line.
*/
@@ -302,7 +336,6 @@ public class FieldPanel extends JPanel
* Returns true if the given field location is rendered on the screen; false if scrolled
* offscreen
*
* @param location the location to check
* @return true if the location is on the screen
*/
public boolean isLocationVisible(FieldLocation location) {
@@ -1088,6 +1121,9 @@ public class FieldPanel extends JPanel
}
private void notifyScrollListenerViewChangedAndRepaint() {
if (accessibleFieldPanel != null) {
accessibleFieldPanel.updateLayouts();
}
BigInteger startIndex = BigInteger.ZERO;
BigInteger endIndex = startIndex;
int startY = 0;
@@ -1270,7 +1306,6 @@ public class FieldPanel extends JPanel
/**
* Finds the layout containing the given y position.
*
* @param y the y position.
* @return the layout.
*/
AnchoredLayout findLayoutAt(int y) {
@@ -1314,6 +1349,10 @@ public class FieldPanel extends JPanel
for (FieldSelectionListener l : selectionListeners) {
l.selectionChanged(currentSelection, trigger);
}
if (accessibleFieldPanel != null) {
accessibleFieldPanel.selectionChanged(currentSelection, trigger);
}
}
/**
@@ -1337,9 +1376,148 @@ public class FieldPanel extends JPanel
}
}
@Override
public AccessibleContext getAccessibleContext() {
if (accessibleFieldPanel == null) {
accessibleFieldPanel = new AccessibleFieldPanel();
if (fieldDescriptionProvider != null) {
accessibleFieldPanel.setFieldDescriptionProvider(fieldDescriptionProvider);
}
}
return accessibleFieldPanel;
}
//==================================================================================================
// Inner Classes
//==================================================================================================
// We are forced to declare this as an inner class because AccessibleJComponent is a
// non-static inner class. So this is just a stub and defers all its logic to
// the AccessibleFieldPanelDelegate.
class AccessibleFieldPanel extends AccessibleJComponent implements AccessibleText {
private AccessibleFieldPanelDelegate delegate;
AccessibleFieldPanel() {
delegate = new AccessibleFieldPanelDelegate(layouts, this, FieldPanel.this);
}
public void cursorChanged(FieldLocation newCursorLoc, EventTrigger trigger) {
delegate.setCaret(newCursorLoc, trigger);
}
public void selectionChanged(FieldSelection currentSelection, EventTrigger trigger) {
delegate.setSelection(currentSelection, trigger);
}
public void setFieldDescriptionProvider(FieldDescriptionProvider provider) {
delegate.setFieldDescriptionProvider(provider);
}
@Override
public String getAccessibleDescription() {
return delegate.getFieldDescription();
}
@Override
public String getAccessibleName() {
return name;
}
@Override
public AccessibleText getAccessibleText() {
return this;
}
@Override
public AccessibleStateSet getAccessibleStateSet() {
AccessibleStateSet accessibleStateSet = super.getAccessibleStateSet();
accessibleStateSet.add(AccessibleState.EDITABLE);
accessibleStateSet.add(AccessibleState.MULTI_LINE);
accessibleStateSet.add(AccessibleState.MANAGES_DESCENDANTS);
return accessibleStateSet;
}
@Override
public int getAccessibleChildrenCount() {
return delegate.getFieldCount();
}
@Override
public Accessible getAccessibleChild(int i) {
AccessibleField field = delegate.getAccessibleField(i);
return field;
}
@Override
public Accessible getAccessibleAt(Point p) {
return delegate.getAccessibleAt(p);
}
public void updateLayouts() {
delegate.setLayouts(layouts);
}
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.TEXT;
}
@Override
public int getIndexAtPoint(Point p) {
return delegate.getIndexAtPoint(p);
}
@Override
public Rectangle getCharacterBounds(int i) {
return delegate.getCharacterBounds(i);
}
@Override
public int getCharCount() {
return delegate.getCharCount();
}
@Override
public int getCaretPosition() {
return delegate.getCaretPosition();
}
@Override
public String getAtIndex(int part, int index) {
return delegate.getAtIndex(part, index);
}
@Override
public String getAfterIndex(int part, int index) {
return delegate.getAfterIndex(part, index);
}
@Override
public String getBeforeIndex(int part, int index) {
return delegate.getBeforeIndex(part, index);
}
@Override
public AttributeSet getCharacterAttribute(int i) {
// currently unsupported
return null;
}
@Override
public int getSelectionStart() {
return delegate.getSelectionStart();
}
@Override
public int getSelectionEnd() {
return delegate.getSelectionEnd();
}
@Override
public String getSelectedText() {
return delegate.getSelectedText();
}
}
private class FieldPanelMouseAdapter extends MouseAdapter {
@@ -1448,39 +1626,71 @@ public class FieldPanel extends JPanel
}
}
private class TabRightAction implements KeyAction {
@Override
public void handleKeyEvent(KeyEvent event) {
keyHandler.vkTab(event);
}
}
private class TabLeftAction implements KeyAction {
@Override
public void handleKeyEvent(KeyEvent event) {
keyHandler.vkShiftTab(event);
}
}
private class FieldPanelKeyAdapter extends KeyAdapter {
private Map<KeyStroke, KeyAction> actionMap;
FieldPanelKeyAdapter() {
actionMap = new HashMap<>();
int shift = InputEvent.SHIFT_DOWN_MASK;
int control = DockingUtils.CONTROL_KEY_MODIFIER_MASK;
//
// Arrow Keys
// Arrow Keys (with shift pressed or not
//
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new UpKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new DownKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new LeftKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new RightKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, shift), new UpKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, shift), new DownKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, shift), new LeftKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, shift), new RightKeyAction());
//
// Home/End and Control/Command Home/End
// Tab Keys
//
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), new TabRightAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, shift), new TabLeftAction());
//
// Home/End and Control/Command Home/End (with shift pressed or not)
//
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), new HomeKeyAction());
actionMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_HOME, DockingUtils.CONTROL_KEY_MODIFIER_MASK),
new HomeKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, control), new HomeKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), new EndKeyAction());
actionMap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_END, DockingUtils.CONTROL_KEY_MODIFIER_MASK),
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, control), new EndKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, shift), new HomeKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, shift | control),
new HomeKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, shift), new EndKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, shift | control),
new EndKeyAction());
//
// Page Up/Down
// Page Up/Down (with the shift pressed or not)
//
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), new PageUpKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
new PageDownKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new EnterKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, shift),
new PageUpKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, shift),
new PageDownKeyAction());
actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shift), new EnterKeyAction());
}
@@ -1491,10 +1701,9 @@ public class FieldPanel extends JPanel
return; // let ALT-?? be used for other action key bindings
}
// Shift is handled special, so mask it off in the event before getting the action.
// If the shift is being held, the selection is extended while moving the cursor.
int keyCode = e.getKeyCode();
int modifiers = e.getModifiersEx() & ~InputEvent.SHIFT_DOWN_MASK;
int modifiers = e.getModifiersEx();
KeyEvent maskedEvent = new KeyEvent(e.getComponent(), e.getID(), e.getWhen(), modifiers,
keyCode, e.getKeyChar(), e.getKeyLocation());
@@ -1749,6 +1958,16 @@ public class FieldPanel extends JPanel
selectionHandler.updateSelectionSequence(cursorPosition);
}
void vkTab(KeyEvent e) {
selectionHandler.endSelectionSequence();
cursorHandler.doTabRight(EventTrigger.GUI_ACTION);
}
void vkShiftTab(KeyEvent e) {
selectionHandler.endSelectionSequence();
cursorHandler.doTabLeft(EventTrigger.GUI_ACTION);
}
void vkEnd(KeyEvent e) {
if (DockingUtils.isControlModifier(e)) {
doEndOfFile(EventTrigger.GUI_ACTION);
@@ -1996,6 +2215,57 @@ public class FieldPanel extends JPanel
return true;
}
private boolean doTabRight(EventTrigger trigger) {
if (!cursorOn) {
// if no cursor, nothing to tab from or to
return false;
}
scrollToCursor();
Layout layout = findLayoutOnScreen(cursorPosition.getIndex());
if (layout == null) {
return false;
}
int numFields = layout.getNumFields();
if (cursorPosition.fieldNum < numFields - 1) {
doSetCursorPosition(cursorPosition.getIndex(), cursorPosition.fieldNum + 1, 0, 0,
trigger);
}
else {
BigInteger indexAfter = getIndexAfter(cursorPosition.getIndex());
if (indexAfter == null) {
return false;
}
doSetCursorPosition(indexAfter, 0, 0, 0, trigger);
}
scrollToCursor();
repaint();
return true;
}
private boolean doTabLeft(EventTrigger trigger) {
if (!cursorOn) {
// if no cursor, nothing to tab from or to
return false;
}
if (cursorPosition.fieldNum > 0) {
doSetCursorPosition(cursorPosition.getIndex(), cursorPosition.fieldNum - 1, 0, 0,
trigger);
}
else {
BigInteger indexBefore = getIndexBefore(cursorPosition.getIndex());
if (indexBefore == null) {
return false;
}
Layout layout = model.getLayout(indexBefore);
int fieldNum = layout.getNumFields() - 1;
doSetCursorPosition(indexBefore, fieldNum, 0, 0, trigger);
}
scrollToCursor();
repaint();
return true;
}
private boolean doCursorUp(EventTrigger trigger) {
if (!cursorOn) {
scrollLineUp();
@@ -2157,6 +2427,9 @@ public class FieldPanel extends JPanel
}
FieldLocation currentLocation = new FieldLocation(cursorPosition);
if (accessibleFieldPanel != null) {
accessibleFieldPanel.cursorChanged(currentLocation, trigger);
}
for (FieldLocationListener l : cursorListeners) {
l.fieldLocationChanged(currentLocation, currentField, trigger);
}
@@ -190,4 +190,12 @@ public interface Layout {
* @return the smallest possible width of this layout that can display its full contents
*/
int getCompressableWidth();
/**
* Returns the index of the field at the given coordinates (relative to the layout)
* @param x the x coordinate
* @param y the y coordinate
* @return the index of the field at the given coordinates (relative to the layout)
*/
int getFieldIndex(int x, int y);
}
@@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -204,4 +203,9 @@ public class AnchoredLayout implements Layout {
cursorLoc.setIndex(index);
return layout.setCursor(cursorLoc, x, y - yPos);
}
@Override
public int getFieldIndex(int x, int y) {
return layout.getFieldIndex(x, y - yPos);
}
}
@@ -149,6 +149,18 @@ public class MultiRowLayout implements Layout {
return layouts[0].setCursor(cursorLoc, x, y);
}
@Override
public int getFieldIndex(int x, int y) {
int offset = 0;
for (int i = 0; i < layouts.length; i++) {
if (layouts[i].contains(y - offset)) {
return layouts[i].getFieldIndex(x, y - offset) + offsets[i];
}
offset += layouts[i].getHeight();
}
return layouts[0].getFieldIndex(x, y);
}
@Override
public Rectangle getCursorRect(int fieldNum, int row, int col) {
int offset = 0;
@@ -201,9 +201,7 @@ public class RowLayout implements Layout {
if (index < 0) {
index = 0;
}
Field field = fields[index];
// y passed-in is 0-based; update y to be relative to our starting position, which is
// the tallest field in this group of fields, using that field's height above its font
// baseline.
@@ -381,9 +379,9 @@ public class RowLayout implements Layout {
* Finds the most appropriate field to place the cursor for the given horizontal
* position. If the position is between fields, first try to the left and if that
* doesn't work, try to the right.
* @param x the x value
* @param y the y value
* @return the index
* @param x the x coordinate relative to the field panel
* @param y the y coordinate relative to the field panel
* @return the index
*/
int findAppropriateFieldIndex(int x, int y) {
y -= heightAbove;
@@ -441,4 +439,10 @@ public class RowLayout implements Layout {
public int getEndRowFieldNum(int field2) {
return getNumFields();
}
@Override
public int getFieldIndex(int x, int y) {
int index = this.findAppropriateFieldIndex(x, y);
return index >= 0 ? index : 0;
}
}
@@ -0,0 +1,379 @@
/* ###
* IP: GHIDRA
*
* Licensed 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.fieldpanel;
import static org.junit.Assert.*;
import java.awt.*;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.accessibility.*;
import javax.swing.JLabel;
import javax.swing.JPanel;
import org.junit.Before;
import org.junit.Test;
import docking.widgets.EventTrigger;
import docking.widgets.fieldpanel.field.*;
import docking.widgets.fieldpanel.support.*;
public class AccessibleFieldPanelDelegateTest {
private static final int FIELD_WIDTH = 100;
private static final int FIELD_HEIGHT = 100;
private AccessibleFieldPanelDelegate delegate;
private static FontMetrics fontMetrics =
new JLabel("Dummy").getFontMetrics(new Font("Monospaced", Font.PLAIN, 12));
private List<AnchoredLayout> layouts;
private JPanel panel = new JPanel();
private TestAccessibleContext testContext = new TestAccessibleContext();
private int fieldLineHeight = fontMetrics.getHeight() + 1;
@Before
public void setup() {
layouts = List.of(buildAnchoredLayout(0, 0, 3), buildAnchoredLayout(1, FIELD_HEIGHT, 13));
delegate = new AccessibleFieldPanelDelegate(layouts, testContext, panel);
delegate.setFieldDescriptionProvider(new TestFieldDescriptionProvider());
delegate.setCaret(new FieldLocation(BigInteger.ZERO, 0, 0, 0), EventTrigger.API_CALL);
}
@Test
public void testGetChildrenCount() {
layouts = List.of(buildAnchoredLayout(0, 0, 3), buildAnchoredLayout(1, FIELD_HEIGHT, 5));
delegate.setLayouts(layouts);
assertEquals(8, delegate.getFieldCount());
}
@Test
public void testGetAccessibleChildFromOrdinal() {
assertEquals("Field 0, 0", getId(delegate.getAccessibleField(0)));
assertEquals("Field 0, 1", getId(delegate.getAccessibleField(1)));
assertEquals("Field 0, 2", getId(delegate.getAccessibleField(2)));
assertEquals("Field 1, 0", getId(delegate.getAccessibleField(3)));
assertEquals("Field 1, 1", getId(delegate.getAccessibleField(4)));
assertEquals("Field 1, 11", getId(delegate.getAccessibleField(14)));
assertEquals(null, delegate.getAccessibleField(16));
assertEquals(null, delegate.getAccessibleField(-1));
}
private String getId(AccessibleField field) {
String text = field.getText();
int indexOf = text.indexOf(":");
return text.substring(0, indexOf);
}
@Test
public void testGetAccessibleChildFromFieldLocation() {
assertEquals("Field 0, 0", getId(delegate.getAccessibleField(fieldLoc(0, 0))));
assertEquals("Field 0, 1", getId(delegate.getAccessibleField(fieldLoc(0, 1))));
assertEquals("Field 0, 2", getId(delegate.getAccessibleField(fieldLoc(0, 2))));
assertEquals("Field 1, 0", getId(delegate.getAccessibleField(fieldLoc(1, 0))));
assertEquals("Field 1, 1", getId(delegate.getAccessibleField(fieldLoc(1, 1))));
assertEquals("Field 1, 2", getId(delegate.getAccessibleField(fieldLoc(1, 2))));
assertEquals("Field 1, 12", getId(delegate.getAccessibleField(fieldLoc(1, 12))));
assertEquals(null, delegate.getAccessibleField(fieldLoc(15, 0)));
assertEquals(null, delegate.getAccessibleField(fieldLoc(-1, 0)));
}
@Test
public void testGetAccessibleChildCache() {
AccessibleField accessibleField1 = delegate.getAccessibleField(0);
assertEquals("Field 0, 0", getId(accessibleField1));
AccessibleField accessibleField2 = delegate.getAccessibleField(0);
assertEquals("Field 0, 0", getId(accessibleField1));
assertTrue(accessibleField1 == accessibleField2);
}
@Test
public void testGetAccessbileAt() {
AccessibleField accessibleField =
(AccessibleField) delegate.getAccessibleAt(new Point(0, 0));
assertEquals("Field 0, 0", getId(accessibleField));
accessibleField = (AccessibleField) delegate.getAccessibleAt(new Point(210, 0));
assertEquals("Field 0, 2", getId(accessibleField));
accessibleField = (AccessibleField) delegate.getAccessibleAt(new Point(220, 112));
assertEquals("Field 1, 2", getId(accessibleField));
}
@Test
public void testGetFieldDescription() {
assertEquals("Description for field: 0, 0", delegate.getFieldDescription());
delegate.setCaret(new FieldLocation(BigInteger.ONE, 2, 0, 0), EventTrigger.API_CALL);
assertEquals("Description for field: 1, 2", delegate.getFieldDescription());
}
@Test
public void testGetCaretPosition() {
assertEquals(0, delegate.getCaretPosition());
delegate.setCaret(new FieldLocation(BigInteger.ONE, 2, 0, 3), EventTrigger.API_CALL);
assertEquals(3, delegate.getCaretPosition());
}
@Test
public void testGetCharCount() {
// the first field is "Field 0, 0: line 1\nField 0, 0: line 2", so length is 37
assertEquals(37, delegate.getCharCount());
delegate.setCaret(fieldLoc(1, 11), EventTrigger.API_CALL);
// the active field is now "Field 1, 10: line 1 Field 1,10: line 2", so length is 39
assertEquals(39, delegate.getCharCount());
}
@Test
public void testGetCharBounds() {
int row = 0;
int fieldNum = 0;
delegate.setCaret(fieldLoc(row, fieldNum), EventTrigger.API_CALL);
assertEquals(rect(0, 0, 7, fieldLineHeight), delegate.getCharacterBounds(0));
assertEquals(rect(7, 0, 7, fieldLineHeight), delegate.getCharacterBounds(1));
assertEquals(rect(14, 0, 7, fieldLineHeight), delegate.getCharacterBounds(2));
row = 0;
fieldNum = 1;
delegate.setCaret(fieldLoc(row, fieldNum), EventTrigger.API_CALL);
int startX = FIELD_WIDTH * fieldNum;
int startY = FIELD_HEIGHT * row;
assertEquals(rect(startX, startY, 7, fieldLineHeight), delegate.getCharacterBounds(0));
assertEquals(rect(startX + 7, startY, 7, fieldLineHeight), delegate.getCharacterBounds(1));
assertEquals(rect(startX + 14, startY, 7, fieldLineHeight), delegate.getCharacterBounds(2));
row = 1;
fieldNum = 3;
delegate.setCaret(fieldLoc(row, fieldNum), EventTrigger.API_CALL);
startX = FIELD_WIDTH * fieldNum;
startY = FIELD_HEIGHT * row;
assertEquals(rect(startX, startY, 7, fieldLineHeight), delegate.getCharacterBounds(0));
assertEquals(rect(startX + 7, startY, 7, fieldLineHeight), delegate.getCharacterBounds(1));
assertEquals(rect(startX + 14, startY, 7, fieldLineHeight), delegate.getCharacterBounds(2));
}
@Test
public void testGetIndexAtPoint_1stRow1stFieldActive() {
int row = 0;
int fieldNum = 0;
delegate.setCaret(fieldLoc(row, fieldNum), EventTrigger.API_CALL);
// char size is 8 x 16
// second line starts at char 19
// the field starts at 0,0 and contains:
//
// Field 0,0: line 1
// Field 0,0: line 2
assertEquals(0, delegate.getIndexAtPoint(new Point(0, 0)));
assertEquals(0, delegate.getIndexAtPoint(new Point(3, 3)));
assertEquals(1, delegate.getIndexAtPoint(new Point(8, 3)));
assertEquals(11, delegate.getIndexAtPoint(new Point(80, 0)));
assertEquals(0, delegate.getIndexAtPoint(new Point(0, fieldLineHeight - 1)));
assertEquals(19, delegate.getIndexAtPoint(new Point(0, fieldLineHeight)));
assertEquals(19, delegate.getIndexAtPoint(new Point(0, fieldLineHeight + 1)));
}
@Test
public void testGetIndexAtPoint_2ndRow3rdFieldActive() {
int row = 1;
int fieldNum = 2;
delegate.setCaret(fieldLoc(row, fieldNum), EventTrigger.API_CALL);
// char size is 8 x 16
// second line starts at char 19
// field upper left corner is at point 200,100
// the field starts at 0,0 and contains:
//
// Field 0,0: line 1
// Field 0,0: line 2
assertEquals(0, delegate.getIndexAtPoint(new Point(200, 100)));
assertEquals(0, delegate.getIndexAtPoint(new Point(203, 103)));
assertEquals(1, delegate.getIndexAtPoint(new Point(208, 103)));
assertEquals(11, delegate.getIndexAtPoint(new Point(280, 100)));
assertEquals(0, delegate.getIndexAtPoint(new Point(200, 100 + fieldLineHeight - 1)));
assertEquals(19, delegate.getIndexAtPoint(new Point(200, 100 + fieldLineHeight)));
assertEquals(19, delegate.getIndexAtPoint(new Point(200, 100 + fieldLineHeight + 1)));
assertEquals(-1, delegate.getIndexAtPoint(new Point(0, 0)));
}
@Test
public void testGetAtIndex() {
delegate.setCaret(fieldLoc(0, 0), EventTrigger.API_CALL);
assertEquals("F", delegate.getAtIndex(AccessibleText.CHARACTER, 0));
assertEquals("i", delegate.getAtIndex(AccessibleText.CHARACTER, 1));
assertEquals("e", delegate.getAtIndex(AccessibleText.CHARACTER, 2));
assertEquals("1", delegate.getAtIndex(AccessibleText.CHARACTER, 17));
assertEquals("2", delegate.getAtIndex(AccessibleText.CHARACTER, 36));
}
@Test
public void testGetAfterIndex() {
delegate.setCaret(fieldLoc(0, 0), EventTrigger.API_CALL);
assertEquals("i", delegate.getAfterIndex(AccessibleText.CHARACTER, 0));
assertEquals("e", delegate.getAfterIndex(AccessibleText.CHARACTER, 1));
assertEquals("l", delegate.getAfterIndex(AccessibleText.CHARACTER, 2));
assertEquals("1", delegate.getAfterIndex(AccessibleText.CHARACTER, 16));
assertEquals("2", delegate.getAfterIndex(AccessibleText.CHARACTER, 35));
}
@Test
public void testGetBeforeIndex() {
delegate.setCaret(fieldLoc(0, 0), EventTrigger.API_CALL);
assertEquals(null, delegate.getBeforeIndex(AccessibleText.CHARACTER, 0));
assertEquals("F", delegate.getBeforeIndex(AccessibleText.CHARACTER, 1));
assertEquals("i", delegate.getBeforeIndex(AccessibleText.CHARACTER, 2));
assertEquals("1", delegate.getBeforeIndex(AccessibleText.CHARACTER, 18));
assertEquals("2", delegate.getBeforeIndex(AccessibleText.CHARACTER, 37));
}
@Test
public void testGetSelectionStartEndAndText_noSelection() {
delegate.setCaret(fieldLoc(0, 0), EventTrigger.API_CALL);
delegate.setSelection(null, EventTrigger.API_CALL);
assertEquals(0, delegate.getSelectionStart());
assertEquals(0, delegate.getSelectionEnd());
assertEquals(null, delegate.getSelectedText());
}
@Test
public void testGetSelectionStartEndAndText_withSelection() {
delegate.setCaret(fieldLoc(0, 0), EventTrigger.API_CALL);
FieldSelection fieldSelection = new FieldSelection();
fieldSelection.addRange(0, 1);
delegate.setSelection(fieldSelection, EventTrigger.API_CALL);
assertEquals(0, delegate.getSelectionStart());
assertEquals(delegate.getCharCount(), delegate.getSelectionEnd());
assertEquals("Field 0, 0: Line 1 Field 0, 0: Line 2", delegate.getSelectedText());
}
private FieldLocation fieldLoc(int index, int fieldNum) {
return new FieldLocation(BigInteger.valueOf(index), fieldNum, 0, 0);
}
private Rectangle rect(int x, int y, int w, int h) {
return new Rectangle(x, y, w, h);
}
private AnchoredLayout buildAnchoredLayout(int index, int yPos, int numFields) {
return new AnchoredLayout(buildLayout(index, numFields), BigInteger.valueOf(index), yPos);
}
private Layout buildLayout(int index, int numFields) {
return new DummyLayout(index, numFields);
}
private class DummyLayout extends RowLayout {
public DummyLayout(int index, int numFields) {
super(createFields(index, numFields), 0);
}
private static Field[] createFields(int index, int numFields) {
Field[] fields = new Field[numFields];
for (int i = 0; i < numFields; i++) {
fields[i] = new DummyField(index, i);
}
return fields;
}
}
private static class DummyField extends VerticalLayoutTextField {
public DummyField(int index, int fieldNum) {
super(createSubFields(index, fieldNum), fieldNum * FIELD_WIDTH, FIELD_WIDTH, 2, null);
}
private static List<FieldElement> createSubFields(int index, int fieldNum) {
List<FieldElement> list = new ArrayList<>();
String text = "Field " + index + ", " + fieldNum + ": Line 1";
AttributedString as = new AttributedString(text, Color.BLACK, fontMetrics);
list.add(new TextFieldElement(as, 0, 0));
text = "Field " + index + ", " + fieldNum + ": Line 2";
as = new AttributedString(text, Color.BLACK, fontMetrics);
list.add(new TextFieldElement(as, 1, 0));
return list;
}
}
private class TestFieldDescriptionProvider implements FieldDescriptionProvider {
@Override
public String getDescription(FieldLocation loc, Field field) {
return "Description for field: " + loc.getIndex() + ", " + loc.getFieldNum();
}
}
private class TestAccessibleContext extends AccessibleContext {
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.TEXT;
}
@Override
public AccessibleStateSet getAccessibleStateSet() {
return null;
}
@Override
public int getAccessibleIndexInParent() {
return 0;
}
@Override
public int getAccessibleChildrenCount() {
return 0;
}
@Override
public Accessible getAccessibleChild(int i) {
return null;
}
@Override
public Locale getLocale() throws IllegalComponentStateException {
return null;
}
}
}
@@ -0,0 +1,415 @@
/* ###
* IP: GHIDRA
*
* Licensed 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.fieldpanel;
import static org.junit.Assert.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import javax.accessibility.*;
import javax.swing.JLabel;
import javax.swing.JPanel;
import org.junit.Before;
import org.junit.Test;
import docking.widgets.fieldpanel.field.*;
import generic.test.AbstractGenericTest;
public class AccessibleFieldTest extends AbstractGenericTest {
private static final int PARENT_X = 1000;
private static final int PARENT_Y = 1000;
private static final int FIELD_X = 100;
private static final int FIELD_Y = 100;
private static final int FIELD_WIDTH = 75;
private JPanel parent = new JPanel() {
public Point getLocationOnScreen() {
return new Point(PARENT_X, PARENT_Y);
}
};
private TestField testField;
private AccessibleField accessibleField;
private Rectangle boundsInParent;
private int fieldHeight;
@Before
public void setUp() {
testField = new TestField(FIELD_X, FIELD_WIDTH, "line1", "line2");
fieldHeight = testField.getHeight();
boundsInParent = new Rectangle(FIELD_X, FIELD_Y, FIELD_WIDTH, fieldHeight);
accessibleField = new AccessibleField(testField, parent, 0, boundsInParent);
}
@Test
public void testGetName() {
assertEquals("Field", accessibleField.getAccessibleName());
}
@Test
public void testGetAccessibleContext() {
assertEquals(accessibleField, accessibleField.getAccessibleContext());
}
@Test
public void testGetAccessibleText() {
assertEquals(accessibleField, accessibleField.getAccessibleText());
}
@Test
public void testGetAccessibleComponent() {
assertEquals(accessibleField, accessibleField.getAccessibleComponent());
}
@Test
public void testGetAccessibleRole() {
assertEquals(AccessibleRole.TEXT, accessibleField.getAccessibleRole());
}
@Test
public void testAccessibleIndexInParent() {
assertEquals(0, accessibleField.getAccessibleIndexInParent());
accessibleField = new AccessibleField(testField, parent, 5, boundsInParent);
assertEquals(5, accessibleField.getAccessibleIndexInParent());
}
@Test
public void testGetAccessibleStateSet() {
AccessibleStateSet set = accessibleField.getAccessibleStateSet();
assertTrue(set.contains(AccessibleState.MULTI_LINE));
assertTrue(set.contains(AccessibleState.TRANSIENT));
}
@Test
public void testGetLocale() {
assertEquals(parent.getLocale(), accessibleField.getLocale());
}
@Test
public void testGetAccessibleChildCount() {
assertEquals(0, accessibleField.getAccessibleChildrenCount());
}
@Test
public void testGetAccessibleChild() {
assertNull(accessibleField.getAccessibleChild(0));
}
@Test
public void testGetBounds() {
assertEquals(new Rectangle(FIELD_X, FIELD_Y, FIELD_WIDTH, fieldHeight),
accessibleField.getBounds());
}
@Test
public void testGetIndexAtPoint() {
assertEquals(0, accessibleField.getIndexAtPoint(new Point(0, 0)));
assertEquals(3,
accessibleField.getIndexAtPoint(new Point(3 * testField.getCharWidth(), 0)));
assertEquals(6, accessibleField.getIndexAtPoint(new Point(0, testField.getLineHeight())));
}
@Test
public void testGetCharacterBounds() {
// text = "line1 line2"
int charWidth = testField.getCharWidth();
int lineHeight = testField.getLineHeight();
assertEquals(new Rectangle(0, 0, charWidth, lineHeight),
accessibleField.getCharacterBounds(0));
assertEquals(new Rectangle(4 * charWidth, 0, charWidth, lineHeight),
accessibleField.getCharacterBounds(4));
// this is the imaginary space char that separates the lines
assertEquals(new Rectangle(5 * charWidth, 0, 0, lineHeight),
accessibleField.getCharacterBounds(5));
assertEquals(new Rectangle(0, lineHeight, charWidth, lineHeight),
accessibleField.getCharacterBounds(6));
// this is the last char on the 2nd line
assertEquals(new Rectangle(4 * charWidth, lineHeight, charWidth, lineHeight),
accessibleField.getCharacterBounds(10));
// this is just past the last char on the 2nd line
assertEquals(new Rectangle(0, 0, 0, 0), accessibleField.getCharacterBounds(11));
assertEquals(new Rectangle(0, 0, 0, 0), accessibleField.getCharacterBounds(12));
assertEquals(new Rectangle(0, 0, 0, 0), accessibleField.getCharacterBounds(-1));
}
@Test
public void testGetCharCount() {
// text = "line1 line2"
assertEquals(testField.getText().length(), accessibleField.getCharCount());
}
@Test
public void testGetAtIndex_char() {
// text = "line1 line2"
assertEquals("l", accessibleField.getAtIndex(AccessibleText.CHARACTER, 0));
assertEquals("i", accessibleField.getAtIndex(AccessibleText.CHARACTER, 1));
assertEquals("n", accessibleField.getAtIndex(AccessibleText.CHARACTER, 2));
assertEquals("e", accessibleField.getAtIndex(AccessibleText.CHARACTER, 3));
assertEquals("1", accessibleField.getAtIndex(AccessibleText.CHARACTER, 4));
assertEquals(" ", accessibleField.getAtIndex(AccessibleText.CHARACTER, 5));
assertEquals("l", accessibleField.getAtIndex(AccessibleText.CHARACTER, 6));
assertEquals("i", accessibleField.getAtIndex(AccessibleText.CHARACTER, 7));
assertEquals("n", accessibleField.getAtIndex(AccessibleText.CHARACTER, 8));
assertEquals("e", accessibleField.getAtIndex(AccessibleText.CHARACTER, 9));
assertEquals("2", accessibleField.getAtIndex(AccessibleText.CHARACTER, 10));
assertEquals(null, accessibleField.getAtIndex(AccessibleText.CHARACTER, 11));
}
@Test
public void testGetBeforeIndex_char() {
// text = "line1 line2"
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 0));
assertEquals("l", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 1));
assertEquals("i", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 2));
assertEquals("n", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 3));
assertEquals("e", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 4));
assertEquals("1", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 5));
assertEquals(" ", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 6));
assertEquals("l", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 7));
assertEquals("i", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 8));
assertEquals("n", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 9));
assertEquals("e", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 10));
assertEquals("2", accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 11));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.CHARACTER, 12));
}
@Test
public void testGetAfterIndex_char() {
// text = "line1 line2"
assertEquals("i", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 0));
assertEquals("n", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 1));
assertEquals("e", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 2));
assertEquals("1", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 3));
assertEquals(" ", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 4));
assertEquals("l", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 5));
assertEquals("i", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 6));
assertEquals("n", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 7));
assertEquals("e", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 8));
assertEquals("2", accessibleField.getAfterIndex(AccessibleText.CHARACTER, 9));
assertEquals(null, accessibleField.getAtIndex(AccessibleText.CHARACTER, 11));
}
@Test
public void testGetAtIndex_word() {
// text = "line1 line2"
assertEquals("line1", accessibleField.getAtIndex(AccessibleText.WORD, 0));
assertEquals("line1", accessibleField.getAtIndex(AccessibleText.WORD, 1));
assertEquals("line1", accessibleField.getAtIndex(AccessibleText.WORD, 2));
assertEquals("line1", accessibleField.getAtIndex(AccessibleText.WORD, 3));
assertEquals("line1", accessibleField.getAtIndex(AccessibleText.WORD, 4));
assertEquals(" ", accessibleField.getAtIndex(AccessibleText.WORD, 5));
assertEquals("line2", accessibleField.getAtIndex(AccessibleText.WORD, 6));
assertEquals("line2", accessibleField.getAtIndex(AccessibleText.WORD, 7));
assertEquals("line2", accessibleField.getAtIndex(AccessibleText.WORD, 8));
assertEquals("line2", accessibleField.getAtIndex(AccessibleText.WORD, 9));
assertEquals("line2", accessibleField.getAtIndex(AccessibleText.WORD, 10));
assertEquals(null, accessibleField.getAtIndex(AccessibleText.WORD, 11));
}
@Test
public void testGetBeforeIndex_word() {
// text = "line1 line2"
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.WORD, 0));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.WORD, 1));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.WORD, 2));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.WORD, 3));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.WORD, 4));
assertEquals("line1", accessibleField.getBeforeIndex(AccessibleText.WORD, 5));
assertEquals(" ", accessibleField.getBeforeIndex(AccessibleText.WORD, 6));
assertEquals(" ", accessibleField.getBeforeIndex(AccessibleText.WORD, 7));
assertEquals(" ", accessibleField.getBeforeIndex(AccessibleText.WORD, 8));
assertEquals(" ", accessibleField.getBeforeIndex(AccessibleText.WORD, 9));
assertEquals(" ", accessibleField.getBeforeIndex(AccessibleText.WORD, 10));
assertEquals("line2", accessibleField.getBeforeIndex(AccessibleText.WORD, 11));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.WORD, 12));
}
@Test
public void testGetAfterIndex_word() {
// text = "line1 line2"
assertEquals(" ", accessibleField.getAfterIndex(AccessibleText.WORD, 0));
assertEquals(" ", accessibleField.getAfterIndex(AccessibleText.WORD, 1));
assertEquals(" ", accessibleField.getAfterIndex(AccessibleText.WORD, 2));
assertEquals(" ", accessibleField.getAfterIndex(AccessibleText.WORD, 3));
assertEquals(" ", accessibleField.getAfterIndex(AccessibleText.WORD, 4));
assertEquals("line2", accessibleField.getAfterIndex(AccessibleText.WORD, 5));
assertEquals(null, accessibleField.getAfterIndex(AccessibleText.WORD, 6));
assertEquals(null, accessibleField.getAfterIndex(AccessibleText.WORD, 7));
assertEquals(null, accessibleField.getAfterIndex(AccessibleText.WORD, 8));
assertEquals(null, accessibleField.getAfterIndex(AccessibleText.WORD, 9));
assertEquals(null, accessibleField.getAfterIndex(AccessibleText.WORD, 10));
assertEquals(null, accessibleField.getAfterIndex(AccessibleText.WORD, 11));
}
@Test
public void testGetAtIndex_sentence() {
testField = new TestField(FIELD_X, FIELD_WIDTH, "This line. Why?", "Why not? Wow");
accessibleField = new AccessibleField(testField, parent, 0, boundsInParent);
assertEquals("This line. ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 0));
assertEquals("This line. ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 4));
assertEquals("This line. ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 10));
assertEquals("Why? ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 11));
assertEquals("Why? ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 15));
assertEquals("Why not? ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 16));
assertEquals("Why not? ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 19));
assertEquals("Why not? ", accessibleField.getAtIndex(AccessibleText.SENTENCE, 23));
assertEquals(null, accessibleField.getAtIndex(AccessibleText.SENTENCE, 500));
}
@Test
public void testGetBeforeIndex_sentence() {
testField = new TestField(FIELD_X, FIELD_WIDTH, "This line. Why?", "Why not? Wow");
accessibleField = new AccessibleField(testField, parent, 0, boundsInParent);
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 0));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 4));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 10));
assertEquals("This line. ", accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 11));
assertEquals("This line. ", accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 15));
assertEquals("Why? ", accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 16));
assertEquals("Why? ", accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 19));
assertEquals("Why? ", accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 23));
assertEquals(null, accessibleField.getBeforeIndex(AccessibleText.SENTENCE, 500));
}
@Test
public void testAfterIndex_sentence() {
testField = new TestField(FIELD_X, FIELD_WIDTH, "This line. Why?", "Why not? Wow");
accessibleField = new AccessibleField(testField, parent, 0, boundsInParent);
assertEquals("Why? ", accessibleField.getAfterIndex(AccessibleText.SENTENCE, 0));
assertEquals("Why? ", accessibleField.getAfterIndex(AccessibleText.SENTENCE, 4));
assertEquals("Why? ", accessibleField.getAfterIndex(AccessibleText.SENTENCE, 10));
assertEquals("Why not? ", accessibleField.getAfterIndex(AccessibleText.SENTENCE, 11));
assertEquals("Why not? ", accessibleField.getAfterIndex(AccessibleText.SENTENCE, 15));
assertEquals(null, accessibleField.getAfterIndex(AccessibleText.SENTENCE, 500));
}
@Test
public void testCaretPos() {
assertEquals(0, accessibleField.getCaretPosition());
accessibleField.setCaretPos(5);
assertEquals(5, accessibleField.getCaretPosition());
}
@Test
public void testSelection() {
// text = "line1 line2"
assertEquals(0, accessibleField.getSelectionStart());
assertEquals(0, accessibleField.getSelectionEnd());
assertEquals(null, accessibleField.getSelectedText());
accessibleField.setSelected(true);
assertEquals(0, accessibleField.getSelectionStart());
assertEquals(11, accessibleField.getSelectionEnd());
assertEquals("line1 line2", accessibleField.getSelectedText());
}
@Test
public void testContainsPoint() {
assertTrue(accessibleField.contains(new Point(0, 0)));
int width = testField.getWidth();
int height = testField.getHeight();
assertTrue(accessibleField.contains(new Point(width - 1, height - 1)));
assertFalse(accessibleField.contains(new Point(width, height - 1)));
assertFalse(accessibleField.contains(new Point(width - 1, height)));
assertFalse(accessibleField.contains(new Point(-1, 0)));
assertFalse(accessibleField.contains(new Point(0, -1)));
}
@Test
public void testGetLocation() {
Point expectedLocationOnScreen = new Point(FIELD_X, FIELD_Y);
assertEquals(expectedLocationOnScreen, accessibleField.getLocation());
}
@Test
public void testGetLocationOnScreen() {
Rectangle boundsRelativeToParent = accessibleField.getBounds();
Point expectedLocationOnScreen =
new Point(PARENT_X + boundsRelativeToParent.x, PARENT_Y + boundsRelativeToParent.y);
assertEquals(expectedLocationOnScreen, accessibleField.getLocationOnScreen());
}
@Test
public void testGetSize() {
assertEquals(new Dimension(FIELD_WIDTH, fieldHeight), accessibleField.getSize());
}
@Test
public void testGetTextOffset() {
assertEquals(0, accessibleField.getTextOffset(0, 0));
assertEquals(1, accessibleField.getTextOffset(0, 1));
assertEquals(5, accessibleField.getTextOffset(0, 5));
assertEquals(6, accessibleField.getTextOffset(1, 0));
assertEquals(5, accessibleField.getTextOffset(0, 10));
assertEquals(11, accessibleField.getTextOffset(1, 10));
}
private static class TestField extends VerticalLayoutTextField {
private static FontMetrics metrics = createFontMetrics();
private static FontMetrics createFontMetrics() {
Font f = new Font("Monospaced", Font.PLAIN, 12);
JLabel label = new JLabel("Hey");
return label.getFontMetrics(f);
}
public TestField(int startX, int width, String... lines) {
super(createElements(lines), startX, width, lines.length, null);
}
public int getLineHeight() {
return metrics.getHeight() + 1; // our lines are always 1 more than the font
}
public int getCharWidth() {
return metrics.charWidth('a'); // monospace so all chars same width
}
private static List<FieldElement> createElements(String[] lines) {
List<FieldElement> fieldElements = new ArrayList<>();
int row = 0;
for (String line : lines) {
AttributedString as = new AttributedString(line, Color.black, metrics);
fieldElements.add(new TextFieldElement(as, row++, 0));
}
return fieldElements;
}
}
}