/* * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. */ package controls; import com.sun.javafx.perf.PerformanceTracker; import com.sun.javafx.tk.Toolkit; import javafx.animation.AnimationTimer; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.animation.TimelineBuilder; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.event.EventType; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import javafx.stage.StageBuilder; import javafx.stage.WindowEvent; import javafx.util.Callback; import javafx.util.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.value.ChangeListener; /** * TableView scroll test. * * Measure FPS rate of scrolling the TableView UI component. */ public class TabPaneTest extends Application implements EventHandler , Runnable { private static final String CSS_FILENAME = "css/tableview.css"; private static double viewWidth = 800.0; // width of visible area private static double viewHeight = 600.0; // height of visible area private enum CellTypes { REGULAR("default cell") { @Override Object createCellObject(final double value) { return value; } }, @SuppressWarnings({"UnusedDeclaration"}) BUTTON("button in cell") { @Override Object createCellObject(final double value) { return new Button(Double.toString(value)); } }, @SuppressWarnings({"UnusedDeclaration"}) CHECKBOX("checkbox in cell") { @Override Object createCellObject(final double value) { return new CheckBox(Double.toString(value)); } }, CSS("CSS-featured cell, look for " + CSS_FILENAME) { @Override Object createCellObject(final double value) { return new Label(Double.toString(value)); } }; public final String descr; CellTypes(String descr) { this.descr = descr; } abstract Object createCellObject(final double value); } private enum TestModes { AUTO ("Scroll table via selection API"), KEYBOARD ("walking by arrow keys"), SCROLL_DRAG ("dragging the scroll thumb by mouse"), // SCROLL_PG ("scroll visible area by PageUp/PageDown keys"), CHANGE_MULTIPLE("rotate table data by rows"), CHANGE("change data in random cells"), SORT("sort single column"), RESIZE_COLUMN("resize single column "), RESIZE_TABLE("resize whole table"), STATIC_MODE("Just display stuff"); // MOUSE_WHEEL ("point mouse on first row of the tableview and move mouse up and down using the wheel"), // MOUSE_MOVE ("point mouse on first row of the tableview and move mouse up and down"), // MOUSE_CLICK ("MOUSE_MOVE + pressed/clicked event"); public String descr; TestModes(String descr) { this.descr = descr; } } // parameters below can be configured via command line, see output of printHelp() method for details. private static int numColumns = 30; private static int numRows = 1000; // items in list private static int warmupTime = 10; // warm-up phase duration private static int runTime = 20; // measurement phase duration private static boolean debugStatus = false; // turn off debug output private static boolean useHorScrolling = false;// use horizontal scrolling instead of vertical private static int injectionRate = 20; // default rate depends on type of input events (mouse or keyboard) private static int keysPerInjection = 1; // amount of keys, per single injection private static int maxResizeDelta = 100; private static boolean usePulse = false; // use FX pulse timer instead of timeline with given injection rate // private static TestModes testMode = TestModes.SCROLL_DRAG; // private static TestModes testMode = TestModes.RESIZE_TABLE; private static TestModes testMode = TestModes.STATIC_MODE; // private static TestModes testMode = TestModes.currentTableViewRD; private static CellTypes cellType = CellTypes.REGULAR; private static List>> tables = new ArrayList>>(); private static TableView> currentTableView; private static int numTabs = 1; private static int curTab = 0; private static TabPane tabPane; private Stage stage; private Timeline fpsTimeline; private Timeline eventTask; private PerformanceTracker tracker; private final AnimationTimer timer = new AnimationTimer() { @Override public void handle(long l) { run(); } }; private KeyEvent ke_vk_down; private KeyEvent ke_vk_up; private KeyEvent ke_vk_pg_down; private KeyEvent ke_vk_pg_up; private KeyEvent ke; private int lastY; private int direction = 1; private int selectedIndex = 0; private double columnBaseWidth = 0; private final int thumb_top = 50; private boolean isActive = false; static boolean exitArg = false; ///////////////////////////////////////////////////////////////// public void stopTest() { if(isActive) { isActive = false; if(usePulse) { timer.stop(); } if (eventTask != null) { eventTask.stop(); } if (fpsTimeline != null) { fpsTimeline.stop(); } PerformanceTracker.releaseSceneTracker(stage.getScene()); stage = null; } } public void parseArgs(String[] args) { parseCommandLine(args); } public Stage createTestGUI() { if (tabPane == null) { tabPane = new TabPane() { @Override protected void impl_processCSS() { super.impl_processCSS(); if (debugStatus) System.out.println("TabPane impl_processCSS happening TPTPTP"); } @Override protected void layoutChildren() { super.layoutChildren(); if (debugStatus) System.out.println("TabPane layoutChildren happening TPTPTP"); } }; } tabPane.setPrefSize(viewWidth, viewHeight); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); // due to baseline metrics TableView isn't intended to be reinstantiated next iteration. if (tables.isEmpty()) { for (int n = 0; n < numTabs; n++) { final int count = n; final TableView tableView = new TableView>(createLines()) { @Override protected void impl_processCSS() { super.impl_processCSS(); if (debugStatus) System.out.println("impl_processCSS happening on tableView-"+count); } @Override protected void layoutChildren() { super.layoutChildren(); if (debugStatus) System.out.println("layoutChildren happening on tableView-"+count); } }; tableView.setId("TableView-"+n); tableView.getColumns().addAll(createColumns()); tableView.setPrefSize(viewWidth, viewHeight); tableView.setTableMenuButtonVisible(true); tables.add(tableView); Tab tab = new Tab("TableView-"+n); tabPane.getTabs().add(tab); tab.setContent(tableView); } } currentTableView = tables.get(curTab); currentTableData = tableData.get(curTab); System.out.println("tstamp: " + System.nanoTime()/1000000000 + ", tableview data created"); final Scene scene = new Scene(new Group(tabPane), viewWidth, viewHeight); InvalidationListener sceneListener = new InvalidationListener() { @Override public void invalidated(Observable ov) { tabPane.setPrefWidth(scene.getWidth()); tabPane.setPrefHeight(scene.getHeight()); } }; scene.widthProperty().addListener(sceneListener); scene.heightProperty().addListener(sceneListener); if(cellType == CellTypes.CSS) { scene.getStylesheets().add( getClass().getResource(CSS_FILENAME).toString() ); } // Needed to keep TableView from getting confused under FXBenchmarks if (testMode == TestModes.KEYBOARD) { tables.get(curTab).getSelectionModel().select(0); tables.get(curTab).scrollTo(0); } stage = StageBuilder.create() .title("TableView") // workaround: terminate active test when window is closed .onHidden(new EventHandler() { @Override public void handle(WindowEvent event) { if (isActive) { stopTest(); System.out.println("Application window has been suddenly closed. Program terminated."); System.exit(-1); } } }) .scene(scene) .build(); stage.sizeToScene(); tabPane.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener() { public void changed(ObservableValue ov, Number t, Number t1) { //System.out.println("t=" + t + ", t1=" + t1); } }); tracker= PerformanceTracker.getSceneTracker(stage.getScene()); if(debugStatus) { // init measurement timeline if(fpsTimeline == null) { fpsTimeline = TimelineBuilder.create() .cycleCount(Timeline.INDEFINITE) .keyFrames(new KeyFrame(Duration.seconds(1), new EventHandler() { @Override public void handle(ActionEvent e) { debug(" instant FPS: " + tracker.getInstantFPS() + ", average FPS: " + tracker.getAverageFPS()); debug(" instant pulses: " + tracker.getInstantPulses() + ", average pulses: " + tracker.getAveragePulses()); } }) ).build(); } fpsTimeline.playFromStart(); } return stage; } static ObservableList> currentTableData = FXCollections.observableArrayList(); final static List>> tableData = new ArrayList>>(); public ObservableList> createLines() { ObservableList> data = FXCollections.observableArrayList(); for (int row = 0; row < numRows; row++) { List line = new ArrayList(); for (int col = 0; col <= numColumns; col++) { double value = (col == 0) ? (double)row : Math.random() * 1000; line.add(cellType.createCellObject(value)); } data.add(line); } tableData.add(data); return data; } public Collection,Object>> createColumns() { List,Object>> cols = new ArrayList,Object>>(); for (int i = cols.size(); i <= numColumns; i++) { TableColumn,Object> col = new TableColumn,Object>("Col" + i); final int coli = i; col.setCellValueFactory(new Callback,Object>, ObservableValue>() { public ObservableValue call(TableColumn.CellDataFeatures,Object> p) { return new ReadOnlyObjectWrapper(p.getValue().get(coli)); } }); cols.add(col); } return cols; } /* * Setup and run the Timeline for standalone mode */ private void doAutoTimeline() { // prepare timeline for standalone notifications TimelineBuilder.create().keyFrames( new KeyFrame(Duration.ZERO, new EventHandler() { @Override public void handle(ActionEvent e) { debug("Warm-up duration: " + warmupTime + " sec"); debug("Measurement duration: " + runTime + " sec"); System.out.println("Starting warmup for " + warmupTime + " sec..."); } }), new KeyFrame(new Duration(warmupTime * 1000), new EventHandler() { @Override public void handle(ActionEvent e) { debug("Resetting average fps counter"); tracker.resetAverageFPS(); tracker.resetAveragePulses(); System.out.println("Measurement phase for " + runTime + " sec... "); } }), new KeyFrame(new Duration((runTime + warmupTime) * 1000), new EventHandler() { @Override public void handle(ActionEvent e) { System.out.println("\n Score: " + tracker.getAverageFPS() + " Average FPS over " + runTime + "s "); System.out.println("\tinstant pulses: " + tracker.getInstantPulses() + ", average pulses: " + tracker.getAveragePulses()); System.exit(0); } }) ).build().playFromStart(); } public void runTest(){ switch(testMode) { case STATIC_MODE: System.out.println("STATIC MODE **********"); break; case AUTO:{ selectedIndex = 0; direction = -keysPerInjection; currentTableView.scrollTo(selectedIndex); if(keysPerInjection > 1) { currentTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); } } break; case SCROLL_DRAG:{ lastY = thumb_top; direction = 1; currentTableView.scrollTo(0); } break; case KEYBOARD: { ke_vk_pg_up = new KeyEvent(null, currentTableView, KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, KeyCode.PAGE_UP.getName(), KeyCode.PAGE_UP, false, false, false, false); ke_vk_pg_down = new KeyEvent(null, currentTableView, KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, KeyCode.PAGE_DOWN.getName(), KeyCode.PAGE_DOWN, false, false, false, false); if(useHorScrolling) { ke = ke_vk_down = new KeyEvent(null, currentTableView, KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, KeyCode.RIGHT.getName(), KeyCode.RIGHT, false, false, false, false); ke_vk_up = new KeyEvent(null, currentTableView, KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, KeyCode.LEFT.getName(), KeyCode.LEFT, false, false, false, false); } else { ke = ke_vk_down = new KeyEvent(null, currentTableView,KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, KeyCode.DOWN.getName(), KeyCode.DOWN, false, false, false, false); ke_vk_up = new KeyEvent(null, currentTableView, KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, KeyCode.UP.getName(), KeyCode.UP, false, false, false, false); } currentTableView.getSelectionModel().select(0); } break; case RESIZE_COLUMN:{ direction = 1; currentTableView.getColumns().get(0).setResizable(true); if (columnBaseWidth == 0) { columnBaseWidth = currentTableView.getColumns().get(0).getWidth(); } } break; case RESIZE_TABLE:{ stage.setWidth(viewWidth + maxResizeDelta + 50); stage.setHeight(viewHeight + maxResizeDelta + 50); tabPane.setPrefSize(viewWidth, viewHeight); direction = 1; } break; } if (usePulse) { timer.start(); } else { if(eventTask==null) eventTask = TimelineBuilder.create() .cycleCount(Timeline.INDEFINITE) .keyFrames(new KeyFrame(Duration.millis(injectionRate), this)) .build(); eventTask.playFromStart(); } isActive = true; } @Override public void run() { if(isActive && stage != null) { switch (testMode) { case STATIC_MODE: // do nothing for now. break; case AUTO: { // select range and scroll to first selection int size = currentTableView.getItems().size(); selectedIndex += direction; int endPosition = selectedIndex + keysPerInjection; if(endPosition >= size) { endPosition = size-1; direction = -direction; } selectedIndex = endPosition - keysPerInjection; if(selectedIndex < 0) { selectedIndex = 0; direction = -direction; } if(keysPerInjection > 1) { currentTableView.getSelectionModel().clearSelection(); } currentTableView.getSelectionModel().selectRange(selectedIndex,endPosition); currentTableView.scrollTo(selectedIndex); } break; case SCROLL_DRAG: { process_mouse(); } break; case KEYBOARD: { process_keyboard(); } break; case CHANGE: { for(int i=0;i < keysPerInjection; i++) { int row1 = Math.max(0,(int)(Math.random()* (currentTableData.size()-1))); List list1 = currentTableData.get(row1); int col1 = Math.max(0,(int)(Math.random()* (list1.size()-1))); int row2 = Math.max(0,(int)(Math.random()* (currentTableData.size()-1))); List list2 = currentTableData.get(row2); int col2 = Math.max(0,(int)(Math.random()* (list2.size()-1))); Object obj = list1.get(col1); list1.set(col1,list2.get(col2)); list2.set(col2,obj); } } break; case CHANGE_MULTIPLE: { currentTableData.add(currentTableData.remove(0)); } break; case SORT: { // sort first column TableColumn col = currentTableView.getColumns().get(0); col.setSortable(true); col.setSortType(col.getSortType() == TableColumn.SortType.ASCENDING ? TableColumn.SortType.DESCENDING : TableColumn.SortType.ASCENDING); final List,?>>list = currentTableView.getSortOrder(); list.clear(); list.add(col); } break; case RESIZE_COLUMN: { TableColumn tc = currentTableView.getColumns().get(0); double width = tc.getWidth() + direction; if(width >= columnBaseWidth + maxResizeDelta) { direction = -direction; width = columnBaseWidth + maxResizeDelta; } else if(width <= columnBaseWidth ) { direction = -direction; width = columnBaseWidth; } //tc.widthProperty().set(width); currentTableView.resizeColumn(tc,width - tc.getWidth()); } break; case RESIZE_TABLE:{ // double width = currentTableView.getWidth() + direction; double width = tabPane.getWidth() + direction; if(width >= viewWidth + maxResizeDelta) { direction = -direction; width = viewWidth + maxResizeDelta; } else if(width <= viewWidth) { direction = -direction; width = viewWidth; } // currentTableView.setPrefSize(width, viewHeight + width - viewWidth); tabPane.setPrefSize(width, viewHeight + width - viewWidth); //currentTableView.resize(width,viewHeight); } break; default: throw new RuntimeException("Test mode '" + testMode + "' is not implemented"); } } } public void process_keyboard() { if (!currentTableView.isFocused()) { currentTableView.requestFocus(); currentTableView.toFront(); } int lastIdx = currentTableView.getItems().size() - 1; int injectedKeys = 0; while (injectedKeys < keysPerInjection) { int index = currentTableView.getSelectionModel().getSelectedIndex(); // Check for direction change if(index <= 0) { currentTableView.getSelectionModel().select(0); stage.getScene().impl_processKeyEvent(ke_vk_pg_down); index = currentTableView.getSelectionModel().getSelectedIndex(); ke = ke_vk_down; } else if (index == lastIdx) { stage.getScene().impl_processKeyEvent(ke_vk_pg_up); index = currentTableView.getSelectionModel().getSelectedIndex(); ke = ke_vk_up; } int distToEnd = 0; if (ke == ke_vk_down) { distToEnd = lastIdx - index; } else if (ke == ke_vk_up) { distToEnd = index; } else { throw new IllegalStateException("Unexpected ke: "+ ke); } // System.out.println("KeyEvent = "+ke); distToEnd = Math.min(keysPerInjection - injectedKeys, distToEnd); for(int i=0; i < distToEnd; i++) { stage.getScene().impl_processKeyEvent(ke); } injectedKeys += distToEnd; } } private void process_mouse() { if (!currentTableView.isFocused()) { currentTableView.requestFocus(); currentTableView.toFront(); } // Workaround of case when Control is not laid out yet if (currentTableView.getWidth() == 0 || currentTableView.getHeight() == 0) { return; } if (lastY > currentTableView.getHeight() - 100) { direction = -1; } else if (lastY < thumb_top) { direction = 1; } final int x = (int) (currentTableView.getWidth() - 6); doMouseEvent(x, lastY, 0, MouseEvent.MOUSE_MOVED); doMouseEvent(sceneMouseX, sceneMouseY, 1, MouseEvent.MOUSE_PRESSED); lastY += 15 * direction; doMouseEvent(x, lastY, 0, MouseEvent.MOUSE_MOVED); doMouseEvent(sceneMouseX, sceneMouseY, 1, MouseEvent.MOUSE_RELEASED); } // Copied from FXRobot private boolean isButton1Pressed = false; private boolean isButton2Pressed = false; private boolean isButton3Pressed = false; private double sceneMouseX; private double sceneMouseY; private void doMouseEvent(double x, double y, int clickCount, EventType passedType) { final Scene currentScene = stage.getScene(); final double screenMouseX = currentScene.getWindow().getX() + currentScene.getX() + x; final double screenMouseY = currentScene.getWindow().getY() + currentScene.getY() + y; sceneMouseX = x; sceneMouseY = y; MouseButton button = MouseButton.PRIMARY; EventType type = passedType; if (type == MouseEvent.MOUSE_PRESSED || type == MouseEvent.MOUSE_RELEASED) { boolean pressed = type == MouseEvent.MOUSE_PRESSED; if (button == MouseButton.PRIMARY) { isButton1Pressed = pressed; } else if (button == MouseButton.MIDDLE) { isButton2Pressed = pressed; } else if (button == MouseButton.SECONDARY) { isButton3Pressed = pressed; } } else if (type == MouseEvent.MOUSE_MOVED) { boolean someButtonPressed = isButton1Pressed || isButton2Pressed || isButton3Pressed; if (someButtonPressed) { type = MouseEvent.MOUSE_DRAGGED; button = MouseButton.NONE; } } final MouseEvent e = MouseEvent.impl_mouseEvent( (int) sceneMouseX, (int) sceneMouseY, (int) screenMouseX, (int) screenMouseY, button, clickCount, false, false, false, false, button == MouseButton.SECONDARY, isButton1Pressed, isButton2Pressed, isButton3Pressed, false, type ); Toolkit.getToolkit().defer(new Runnable() { public void run() { currentScene.impl_processMouseEvent(e); } }); } ///////////////////////////////////////////////////////////////// /** * Debug output. * @param msg message for output */ static void debug(String msg) { if (debugStatus) { System.out.println("DEBUG: " + msg); } } /** * Debug output. * @param th exception instance. */ static void debug(Throwable th) { if (debugStatus) { System.out.println("\nDEBUG: " + th.getClass().getCanonicalName() + ": " +th.getMessage()); for(StackTraceElement s: th.getStackTrace()) { System.out.println("DEBUG: " + s.toString()); } } } @Override public void handle(ActionEvent e) { Platform.runLater( this ); } private static void parseCommandLine(String[] args) { for(int i = 0; i < args.length; ++i) { if(i + 1 : set desired test mode, default is "+testMode+"\n" +modeTypes.toString() +"\t-viewsize : set width and height of TableView component (default: "+ viewWidth +"x"+ viewHeight +")\n" +"\t-warmup : warm-up time, seconds (default: "+warmupTime+" sec)\n" +"\t-duration : measurement time, seconds (default: "+runTime+" sec)\n" +"\t-usePulse {true|false} : use FX pulses instead of injection rate timer. (default: "+usePulse+"\n" +"\t-injectionRate : desired millsecond delay between input events, >=1 (default:" +injectionRate+")\n" +"\t-keysPerInjection : amount of key presses per injection, >=1, (default "+keysPerInjection+"\n" +"\t-max-resize-delta : maximum column/table width change for resizing tests, >= 50 (default "+maxResizeDelta+"\n" // +"\t-horizontal {true|false} : true to horizontal scrolling, else processing vertical scrolling\n" +"\t-cells : set size of cells matrix (default: "+numRows + "x"+ numColumns +"\n" +"\t-celltype : select desired shape of cell (default: "+cellType+"\n" +cellTypes.toString() +"\t-debug true : enable debug messages (default "+debugStatus+")\n" +"\n" +"\tExample:\n" +"\t\tjava -jar TableView.jar -mode "+ TestModes.KEYBOARD +" -usePulse true"); System.exit(0); } /* * Application class for running standalone. */ @Override public void start(Stage primaryStage) { Stage stage = createTestGUI(); if (exitArg) { Runnable r = new Runnable() { @Override public void run() { // Due to RT-10458 "remove System.exit from Quantum toolkit" we need // to call System.exit by ourself. Otherwise calling Platform.exit() // results in extra 300ms to shutdown the application which is not what // we want to measure for startup. System.out.println("tstamp: " + System.nanoTime()/1000000000 + ", rendered first frame"); System.exit(0); } }; tracker.setOnRenderedFrameTask(r); } stage.getScene().setOnKeyReleased(new EventHandler() { @Override public void handle(KeyEvent t) { if (t.getCode() == KeyCode.ESCAPE) { System.out.println("Escape key pressed. Application terminated."); System.exit(0); } } }); stage.show(); doAutoTimeline(); runTest(); } /** * Standalone entry point. * @param args Commandline arguments */ public static void main(String[] args) { parseCommandLine(args); Application.launch(TabPaneTest.class, args); } }