diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingFieldDescriptionProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingFieldDescriptionProvider.java new file mode 100644 index 0000000000..0113c256eb --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingFieldDescriptionProvider.java @@ -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"; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java index e4da6deabf..3b3706d5db 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ListingPanel.java @@ -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 highlightProviders = this.formatManager.getHighlightProviders(); + List highlightProviders = + this.formatManager.getHighlightProviders(); this.formatManager = formatManager; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java index 44f6a7ee12..c2eb8ecb68 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/options/OptionsGui.java @@ -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; } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java index b0f4cfb0aa..39bf2b9c3b 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java @@ -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; diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java index f3ad5f7bbf..0867d952b2 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java @@ -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); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java index 649866b193..a3f00c1c7d 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java @@ -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()); } /** diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/vertex/FGVertexListingPanel.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/vertex/FGVertexListingPanel.java index b306d43013..174284b266 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/vertex/FGVertexListingPanel.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/vertex/FGVertexListingPanel.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/ActionAdapter.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/ActionAdapter.java index fab4f54af6..9fc5f10067 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/ActionAdapter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/ActionAdapter.java @@ -43,7 +43,7 @@ public class ActionAdapter implements Action, PropertyChangeListener { *

Most clients should use {@link #ActionAdapter(DockingActionIf, ActionContextProvider)} * @param dockingAction the action to adapt */ - ActionAdapter(DockingActionIf dockingAction) { + public ActionAdapter(DockingActionIf dockingAction) { this(dockingAction, null); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/AccessibleField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/AccessibleField.java new file mode 100644 index 0000000000..298384c14f --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/AccessibleField.java @@ -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 + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/AccessibleFieldPanelDelegate.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/AccessibleFieldPanelDelegate.java new file mode 100644 index 0000000000..5724595c66 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/AccessibleFieldPanelDelegate.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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 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 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 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(); + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldDescriptionProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldDescriptionProvider.java new file mode 100644 index 0000000000..3206f926d2 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldDescriptionProvider.java @@ -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); +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java index ac4f6594c3..5659bf6751 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java @@ -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 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); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/Layout.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/Layout.java index 3246339dd1..63112cd1ce 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/Layout.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/Layout.java @@ -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); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/AnchoredLayout.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/AnchoredLayout.java index 8c10b5d456..0643112f5a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/AnchoredLayout.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/AnchoredLayout.java @@ -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); + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/MultiRowLayout.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/MultiRowLayout.java index 67aff7150b..41deea1f8a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/MultiRowLayout.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/MultiRowLayout.java @@ -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; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java index 2ce98bfc48..16ade7c8e7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/RowLayout.java @@ -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; + } } diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/AccessibleFieldPanelDelegateTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/AccessibleFieldPanelDelegateTest.java new file mode 100644 index 0000000000..d610c298fc --- /dev/null +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/AccessibleFieldPanelDelegateTest.java @@ -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 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 createSubFields(int index, int fieldNum) { + List 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; + } + + } + +} diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/AccessibleFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/AccessibleFieldTest.java new file mode 100644 index 0000000000..678bb9ab63 --- /dev/null +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/fieldpanel/AccessibleFieldTest.java @@ -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 createElements(String[] lines) { + List 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; + } + } + +}