java-topology/test/jdk/javax/swing/JScrollPane/AcceleratedWheelScrolling/RTLScrollers.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

680 lines
22 KiB
Java

/*
* Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
// A few Swing components which use the mouse wheel to scroll
import java.awt.AWTException;
import java.awt.Color;
import java.awt.ComponentOrientation;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.lang.reflect.InvocationTargetException;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.ListModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
public class RTLScrollers extends JDialog
implements MouseWheelListener, ActionListener {
private static final int ROWS = 5;
private static final int COLUMNS = 150;
private static final int WINWIDTH = 1000;
static RTLScrollers rtl;
static volatile RTLScrollers f;
static volatile boolean retVal;
static volatile JScrollPane jsp;
static volatile JScrollBar hsb;
static volatile JScrollBar sb;
static volatile Point loc;
static volatile Dimension size;
TestList list;
JScrollPane listScroller;
JTextArea text;
JScrollPane textScroller;
TestTable table;
JScrollPane tableScroller;
JCheckBoxMenuItem rightToLeft;
ImageIcon photoIcon;
int scrollAmount;
private static Robot robot;
private static BufferedImage logo = genIcon(166, 39, Color.PINK);
private static BufferedImage photo = genIcon(59, 80, Color.MAGENTA);
private static BufferedImage photo2 = genIcon(80, 53, Color.ORANGE);
private static BufferedImage genIcon(int width, int height, Color color) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
g.setColor(color);
g.fillRect(0, 0, width, height);
return image;
}
public RTLScrollers() {
this(0);
}
public RTLScrollers(int scrollAmount) {
super(new JFrame(), "RTLScrollers", false);
this.scrollAmount = scrollAmount;
Container content = getContentPane();
content.setLayout(new GridBagLayout());
DefaultListModel<Object> listModel = new DefaultListModel<>();
photoIcon = new ImageIcon(photo);
for (int i = 0; i < COLUMNS / 4 ; i++) {
for (int j = 0; j < ROWS; j++) {
listModel.addElement(new ImageIcon(logo));
}
for (int j = 0; j < ROWS; j++) {
listModel.addElement(photoIcon);
}
for (int j = 0; j < ROWS; j++) {
listModel.addElement(new ImageIcon(photo2));
}
for (int j = 0; j < ROWS; j++) {
listModel.addElement("Text Item " + ((1 + i) * 3));
}
}
list = new TestList(listModel);
list.setVisibleRowCount(ROWS);
list.setLayoutOrientation(JList.VERTICAL_WRAP);
listScroller = new JScrollPane(list);
listScroller.addMouseWheelListener(this);
text = new JTextArea();
for (int j = 0; j < ROWS ; j++) {
for (int i = 0; i < COLUMNS ; i++) {
text.append(i + " some text, some more text, a really long line of text ");
}
text.append("\n");
}
textScroller = new JScrollPane(text);
textScroller.addMouseWheelListener(this);
DefaultTableModel model = new DefaultTableModel(ROWS, COLUMNS);
table = new TestTable(model);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
for (int i = 0; i < COLUMNS; i++) {
for (int j = 0; j < ROWS; j++) {
table.setValueAt(j + ", " + i, j, i);
}
TableColumn column = table.getColumnModel().getColumn(i);
if (i % 4 == 0) {
column.setMinWidth(0);
column.setPreferredWidth(0);
column.setMaxWidth(0);
}
else if ((i + 1) % 4 == 0) {
column.setMinWidth(95);
column.setPreferredWidth(95);
column.setMaxWidth(95);
}
else if ((i + 2) % 4 == 0) {
column.setMinWidth(26);
column.setPreferredWidth(26);
column.setMaxWidth(26);
}
else {
column.setMinWidth(50);
column.setPreferredWidth(50);
column.setMaxWidth(50);
}
}
tableScroller = new JScrollPane(table);
tableScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
tableScroller.addMouseWheelListener(this);
GridBagConstraints tableGBC = new GridBagConstraints(0,
0,
1,
1,
1.0,
0.3,
GridBagConstraints.CENTER,
GridBagConstraints.BOTH,
new Insets(0,0,0,0),
0,
0);
content.add(tableScroller, tableGBC);
GridBagConstraints textGBC = new GridBagConstraints(0,
1,
1,
1,
1.0,
0.3,
GridBagConstraints.CENTER,
GridBagConstraints.BOTH,
new Insets(0,0,0,0),
0,
0);
content.add(textScroller, textGBC);
GridBagConstraints listGBC = new GridBagConstraints(0,
2,
1,
5,
1.0,
1.2,
GridBagConstraints.CENTER,
GridBagConstraints.BOTH,
new Insets(0,0,0,0),
0,
0);
content.add(listScroller, listGBC);
rightToLeft = new JCheckBoxMenuItem("Right-To-Left", false);
rightToLeft.addActionListener(this);
JMenu menu = new JMenu("Component Orientation");
menu.add(rightToLeft);
JMenuItem close = new JMenuItem("Close");
close.addActionListener(e -> RTLScrollers.this.setVisible(false));
menu.add(close);
JMenuBar mb = new JMenuBar();
mb.add(menu);
setJMenuBar(mb);
setBounds(0, 0, WINWIDTH, 760);
setLocationRelativeTo(null);
}
public void actionPerformed(ActionEvent e) {
if (rightToLeft.getState()) {
applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
}
else {
applyComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
}
}
public void mouseWheelMoved(MouseWheelEvent e) {
System.out.println("Rotation: " + e.getWheelRotation());
}
public static boolean runTest(int scrollAmount)
throws InterruptedException, InvocationTargetException {
System.out.println("RTLS.runTest()");
if (robot == null) {
try {
robot = new Robot();
robot.setAutoDelay(150);
robot.setAutoWaitForIdle(true);
}
catch (AWTException e) {
e.printStackTrace();
return false;
}
}
robot.delay(1000);
SwingUtilities.invokeAndWait(() -> {
rtl = new RTLScrollers(scrollAmount);
rtl.setVisible(true);
});
robot.delay(100);
try {
retVal = rtl.runTests(scrollAmount);
} finally {
SwingUtilities.invokeAndWait(() -> {
rtl.setVisible(false);
rtl.dispose();
});
}
robot.delay(100);
System.out.println("RTLS.runTest(): " + retVal);
return retVal;
}
private boolean runTests(int scrollAmount)
throws InterruptedException, InvocationTargetException {
if (robot == null) {
try {
robot = new Robot();
robot.setAutoDelay(150);
robot.setAutoWaitForIdle(true);
}
catch (AWTException e) {
e.printStackTrace();
return false;
}
}
//
// run robot tests
//
robot.delay(500);
System.out.println("Testing Table");
testComp(table, scrollAmount);
System.out.println("Testing List");
testComp(list, scrollAmount);
SwingUtilities.invokeAndWait(() ->
applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT));
robot.delay(100);
System.out.println("Testing RTL Table");
testComp(table, scrollAmount);
System.out.println("Testing RTL List");
testComp(list, scrollAmount);
return true;
}
public boolean testComp(TestTools comp, int scrollAmount)
throws InterruptedException, InvocationTargetException {
// Make sure we start from the beginning
SwingUtilities.invokeAndWait(() -> {
jsp = (JScrollPane)((JComponent)comp).getParent().getParent();
hsb = jsp.getHorizontalScrollBar();
hsb.setValue(hsb.getMinimum());
loc = jsp.getLocationOnScreen();
size = jsp.getSize();
});
int midx = loc.x + size.width / 2;
int midy = loc.y + size.height / 2;
int maxIdx = 0;
robot.mouseMove(midx, midy);
// Don't bother for max scroll w/ RTL JList, because the block increment is broken
if (scrollAmount != 30 || !(comp instanceof TestList)
|| getComponentOrientation().isLeftToRight()) {
scrollToMiddle(jsp, robot);
// check that we're lined up
comp.checkTopCellIsLinedUp();
int startVal = hsb.getValue();
int leadingCell = comp.getLeadingCell().y;
System.out.println("leadingCell is " + leadingCell);
// become unaligned
int width = comp.getLeadingCellWidth();
int midVal = startVal + width / 2;
System.out.println("becoming unaligned: startVal is "
+ startVal + ", midVal is " + midVal);
SwingUtilities.invokeAndWait(() -> hsb.setValue(midVal));
//
// Check partial inc up
//
robot.mouseWheel(-1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, -scrollAmount + 1);
}
comp.checkTopCellIsLinedUp();
//
// Check partial inc down
//
SwingUtilities.invokeAndWait(() -> hsb.setValue(midVal));
robot.delay(100);
robot.mouseWheel(1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, scrollAmount);
}
comp.checkTopCellIsLinedUp();
//
// Check full inc down (3 times)
//
SwingUtilities.invokeAndWait(() -> hsb.setValue(startVal));
leadingCell = comp.getLeadingCell().y;
// Once...
robot.mouseWheel(1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, scrollAmount);
}
comp.checkTopCellIsLinedUp();
// Twice...
robot.mouseWheel(1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, (2 * scrollAmount));
}
comp.checkTopCellIsLinedUp();
// Thrice...
robot.mouseWheel(1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, (3 * scrollAmount));
}
comp.checkTopCellIsLinedUp();
//
// Check full inc up (3 times)
//
leadingCell = comp.getLeadingCell().y;
// Once...
robot.mouseWheel(-1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, -scrollAmount);
}
comp.checkTopCellIsLinedUp();
// Twice...
robot.mouseWheel(-1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, -(2 * scrollAmount));
}
comp.checkTopCellIsLinedUp();
// Thrice...
robot.mouseWheel(-1);
if (scrollAmount == 30) { // hack for max scroll amount
comp.checkTopCellIsMax(maxIdx++);
}
else {
comp.checkTopCellIs(leadingCell, -(3 * scrollAmount));
}
comp.checkTopCellIsLinedUp();
}
//
// Test acceleration for max scrolling
// (this part should still work for RTL JList)
if (scrollAmount == 30) {
SwingUtilities.invokeAndWait(() -> hsb.setValue(hsb.getMinimum()));
robot.delay(100);
robot.mouseWheel(2);
robot.mouseWheel(2);
robot.mouseWheel(2);
if (hsb.getValue() < hsb.getMaximum() - hsb.getVisibleAmount()) {
throw new RuntimeException("Wheel acceleration for max " +
"scrolling doesn't work: hsb value (" + hsb.getValue() +
" < hsb max (" + hsb.getMaximum() +
") - hsb vis (" + hsb.getVisibleAmount() + ")");
}
robot.delay(100);
robot.mouseWheel(-2);
robot.mouseWheel(-2);
robot.mouseWheel(-2);
if (hsb.getValue() > hsb.getMinimum()) {
throw new RuntimeException("Wheel acceleration for max " +
"scrolling doesn't work: hsb value (" +
hsb.getValue() + " > hsb min (" + hsb.getMinimum() + ")");
}
}
return true;
}
class TestTable extends JTable implements TestTools {
final int[] MAXVALS = {23, 67, 67, 89, 111, 89, 66, 45}; //Lookup table
public TestTable(TableModel model) {
super(model);
}
public void checkTopCellIsLinedUp() {
boolean isLeftToRight = getComponentOrientation().isLeftToRight();
Point leading = getLeadingCell();
Rectangle visRect = getVisibleRect();
Rectangle cellRect = getCellRect(leading.x, leading.y, true);
if (isLeftToRight) {
if (cellRect.x != visRect.x) {
throw new RuntimeException("leading cell is not aligned!");
}
}
else {
if (cellRect.x + cellRect.width != visRect.x + visRect.width) {
throw new RuntimeException("leading cell is not aligned!");
}
}
}
public void checkTopCellIs(int col) {
Point cell = getLeadingCell();
if (cell.y != col) {
throw new RuntimeException("leading cell (" + cell.y
+ ") is not " + col);
}
}
/*
* Account for 0-width cells
*
* shift is a non-0 number of visible cells to shift. The shift is
* augmented for zero-width cells, and the new sum is passed into
* checkTopCellIs().
*/
public void checkTopCellIs(int col, int shift) {
if (shift == 0) {
checkTopCellIs(col);
return;
}
int row = getLeadingCell().x;
int newShift = 0;
int foundCells = 0;
int direction = shift > 0 ? 1 : -1;
int index = col;
Rectangle cellRect;
while (foundCells < Math.abs(shift)) {
index += direction;
cellRect = getCellRect(row, index, true);
if (cellRect.width > 0) {
foundCells++;
}
newShift++;
}
checkTopCellIs(col + (direction*newShift));
}
public void checkTopCellIsMax(int idx) {
checkTopCellIs(MAXVALS[idx]);
}
public int getLeadingCellWidth() {
Point leading = getLeadingCell();
Rectangle cellRect = getCellRect(leading.x, leading.y, true);
return cellRect.width;
}
public Point getLeadingCell() {
boolean isLeftToRight = getComponentOrientation().isLeftToRight();
Rectangle visRect = getVisibleRect();
int row = rowAtPoint(visRect.getLocation());
int column;
if (isLeftToRight) {
column = columnAtPoint(visRect.getLocation());
}
else {
column = columnAtPoint(new Point(visRect.x + visRect.width - 1, visRect.y));
}
return new Point(row, column);
}
}
class TestList extends JList implements TestTools {
final int[] MAXVALS = {5, 16, 15, 20, 25, 20, 15, 10 };
public TestList(ListModel model) {
super(model);
}
// Note - implemented in terms of columns
public Point getLeadingCell() {
System.out.println("TL.gLC(): first vis index is "
+ getFirstVisibleIndex());
return new Point(getFirstVisibleIndex() / ROWS,
getFirstVisibleIndex() / ROWS);
}
public void checkTopCellIsLinedUp() {
boolean isLeftToRight = getComponentOrientation().isLeftToRight();
int visIdx = getFirstVisibleIndex();
Rectangle cellRect = getCellBounds(visIdx, visIdx);
Rectangle visRect = getVisibleRect();
if (isLeftToRight) {
if (cellRect.x != visRect.x) {
throw new RuntimeException("leading cell is not aligned!");
}
}
else {
if (cellRect.x + cellRect.width != visRect.x + visRect.width) {
throw new RuntimeException("leading cell is not aligned!");
}
}
}
public void checkTopCellIs(int col) {
int firstVis = getLeadingCell().y;
if (firstVis != col) {
throw new RuntimeException("leading cell ("
+ firstVis + ") is not " + col);
}
}
public void checkTopCellIs(int idx, int shift) {
checkTopCellIs(idx + shift);
}
public void checkTopCellIsMax(int idx) {
checkTopCellIs(MAXVALS[idx]);
}
public int getLeadingCellWidth() {
int visIdx = getFirstVisibleIndex();
Rectangle cellRect = getCellBounds(visIdx, visIdx);
System.out.println("TL.gLCW(): leading cell width is " + cellRect.width);
return cellRect.width;
}
}
private interface TestTools {
/**
* Throws a runtime exception if the top cell isn't lined up
*/
void checkTopCellIsLinedUp();
void checkTopCellIs(int col);
void checkTopCellIs(int col, int shift);
int getLeadingCellWidth();
Point getLeadingCell();
void checkTopCellIsMax(int idx);
}
public void scrollToMiddle(JScrollPane jsp, Robot robot)
throws InterruptedException, InvocationTargetException {
SwingUtilities.invokeAndWait(() -> {
sb = jsp.getHorizontalScrollBar();
loc = sb.getLocationOnScreen();
size = sb.getSize();
});
robot.setAutoDelay(250);
robot.mouseMove(loc.x + size.width / 2,
loc.y + size.height / 2);
robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
SwingUtilities.invokeAndWait(() -> {
if (jsp == listScroller) {
int idx = list.getFirstVisibleIndex();
list.ensureIndexIsVisible(idx);
}
});
}
public static void main(String[] args) throws Exception {
try {
SwingUtilities.invokeAndWait(() -> f = new RTLScrollers());
} finally {
SwingUtilities.invokeAndWait(() -> {
f.setVisible(true);
f.dispose();
});
}
}
}