I want to bind two tableviews together such that they scroll in sync. How do I do that? I am unable to find out how to access the scrollbar of a tableview.
I've made a CSS hack to bind a Tableview with an external scrollbar. One scrollbar controls both tableviews.
An overview of my idea:
Create two tableviews
Make one Vertical scrollbar. Let's call it myScrollbar in this example
Set the min and max of myScrollbar to size of min=0, max=TableView.Items.size()
When the value of myScrollbar changes then call both tableview's scrollTo(int) function
Disable the native vertical scrollbar of the tableview implemented with CSS.
This will give you two tables, both controlled by one external scrollbar (myScrollbar).
Here is the code to hide the scrollbar of a tableview using css:
/* The main scrollbar **track** CSS class */
.mytableview .scroll-bar:vertical .track{
-fx-padding:0px;
-fx-background-color:transparent;
-fx-border-color:transparent;
-fx-background-radius: 0em;
-fx-border-radius:2em;
}
/* The increment and decrement button CSS class of scrollbar */
.mytableview .scroll-bar:vertical .increment-button ,
.mytableview .scroll-bar:vertical .decrement-button {
-fx-background-color:transparent;
-fx-background-radius: 0em;
-fx-padding:0 0 0 0;
}
.mytableview .scroll-bar:vertical .increment-arrow,
.mytableview .scroll-bar:vertical .decrement-arrow
{
-fx-shape: " ";
-fx-padding:0;
}
/* The main scrollbar **thumb** CSS class which we drag every time (movable) */
.mytableview .scroll-bar:vertical .thumb {
-fx-background-color:transparent;
-fx-background-insets: 0, 0, 0;
-fx-background-radius: 2em;
-fx-padding:0px;
}
Then we need to set how to scroll the tableview by using the scrollbar.
scroll.setMax(100); //make sure the max is equal to the size of the table row data.
scroll.setMin(0);
scroll.valueProperty().addListener(new ChangeListener(){
#Override
public void changed(ObservableValue ov, Number t, Number t1) {
//Scroll your tableview according to the table row index
table1.scrollTo(t1.intValue());
table2.scrollTo(t1.intValue());
}
});
http://blog.ngopal.com.np/2012/09/25/how-to-bind-vertical-scroll-in-multi-tableview/
I don't think this is currently possible. TableViewSkin inherits from VirtualContainerBase which has a VirtualFlow field. The VirtualFlow object has two VirtualScrollBar fields, hbar and vbar which is what you're after. I can't see any way of getting to it though.
Interestingly, there is also a private contentWidth field in TableView although this is private. I'm sure that the JFX team are being ultra cautious about opening up too much of the API which is understandable. You could ask to get the contentWidth field opened up as an int property as a feature requestion on the JFX JIRA or openjfx-dev mailing list.
A stop gap measure would be to bind the selected item or index property of the table views selection model.
The easiest way I've found to solve the problem is to bind the valueProperty of the visible an the hidden scrollbars.
// Controller
#FXML private TableView<MyBean> tableLeft;
#FXML private TableView<MyBean> tableRight;
#FXML private ScrollBar scrollBar;
#SuppressWarnings("rawtypes")
private void bindScrollBars(TableView<?> tableView1, TableView<?> tableView2,
ScrollBar scrollBar, Orientation orientation) {
// Get the scrollbar of first table
VirtualFlow vf = (VirtualFlow)tableView1.getChildrenUnmodifiable().get(1);
ScrollBar scrollBar1 = null;
for (final Node subNode: vf.getChildrenUnmodifiable()) {
if (subNode instanceof ScrollBar &&
((ScrollBar)subNode).getOrientation() == orientation) {
scrollBar1 = (ScrollBar)subNode;
}
}
// Get the scrollbar of second table
vf = (VirtualFlow)tableView2.getChildrenUnmodifiable().get(1);
ScrollBar scrollBar2 = null;
for (final Node subNode: vf.getChildrenUnmodifiable()) {
if (subNode instanceof ScrollBar &&
((ScrollBar)subNode).getOrientation() == orientation) {
scrollBar2 = (ScrollBar)subNode;
}
}
// Set min/max of visible scrollbar to min/max of a table scrollbar
scrollBar.setMin(scrollBar1.getMin());
scrollBar.setMax(scrollBar1.getMax());
// bind the hidden scrollbar valueProterty the visible scrollbar
scrollBar.valueProperty().bindBidirectional(scrollBar1.valueProperty());
scrollBar.valueProperty().bindBidirectional(scrollBar2.valueProperty());
}
/*
* This method must be called in Application.start() after the stage is shown,
* because the hidden scrollbars exist only when the tables are rendered
*/
public void setScrollBarBinding() {
bindScrollBars(this.tableLeft, this.tableRight, this.scrollBar, Orientation.VERTICAL);
}
Now you have to call the binding from Application after the stage is shown and the tables are rendered:
// Application
private MyController controller;
#Override
public void start(Stage primaryStage) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(SalesApp.class.getResource("scene.fxml"));
BorderPane root = (BorderPane) fxmlLoader.load();;
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
controller = (MyController) fxmlLoader.getController();
controller.setScrollBarBinding();
} catch(Exception e) {
e.printStackTrace();
}
}
Now the tables should scroll synchronously via mouse, key, or scrollbar.
Have fun, Olaf
Related
I wrote a software for handling Pizza Payments as a school project. For this I create a custom row as a Anchorpane. After loading a few Entries from a MySQL DB, the ListView for showing the possible Pizzas to choose is struggling (showing white space above and under the entries, where the other Entries should be displayed but aren't).
If you want to see the main Pane FXML click here, if you want to see the Custom ListCell Anchorpane just click here.
To see the error visually just click here
Because I have no idea how this custom cells are rendered in detail I have no idea where the error is, so I tried nothing to fix it.
//the adding a new Pizza Row method in the WindowController Class for the main window
private void addPizzaRow(Pizza pizza) {
try {
FXMLLoader loader = new FXMLLoader(new File(WindowController.ROW_FXML).toURI().toURL());
pizzenContr = new RowPizzasController();
loader.setController(pizzenContr);
Pane rootPane = loader.load();
// initialize tab controller
pizzenContr.init(pizza);
this.pizzenListview.getItems().add(rootPane);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
//the called init() method
public void init(Pizza pizza) {
pizzaImageview.setImage(new Image("Classdependencies/Window/PizzaListViewImg.png"));
//set title and image (icon)
this.pizzaLabel.setText(pizza.getName());
this.kleinButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
}
});
After five or 6 Entries white space above and under the entries is showing, where the other Entries should be displayed but aren't.
Looks like the error had something todo with absolute height I gave the cells Panes after I set this to default in Scenebuilder it worked fine again.
I want the item spacing to appear more orderly with a constant spacing
How can I do this? I combed through the JavaFX CSS properties and modena css, and I didn't see anything relevant that seemed to work. I only find these properties:
.chart-legend {
-fx-background-color: white
-fx-background-insets: 0 0 -1 0, 0,1;
-fx-background-radius: 4,4,3;
-fx-padding: 6px;
}
The legend of a Chart is an arbitrary node. It's default implementation is a specialized TilePane, that is its children are sized uniformly across the available width. For a different layout that default implementation can be replaced with a custom legend, f.i. a FlowPane (or HBox).
A quick approach is to subclass LineChart, override updateLegend and replace the default with a custom pane. The example below is dirty in that it relies on implementation details of the default implementation
The custom LineChart:
public static class MyLineChart<X, Y> extends LineChart<X, Y> {
public MyLineChart(Axis<X> xAxis, Axis<Y> yAxis) {
super(xAxis, yAxis);
}
private TilePane legendAlias;
private FlowPane legendReplacement;
#Override
protected void updateLegend() {
// let super do the setup
super.updateLegend();
Node legend = getLegend();
if (legend instanceof TilePane) {
legendAlias = (TilePane) legend;
legendReplacement = new FlowPane(10, 10);
setLegend(legendReplacement);
}
if (legendAlias != null && legendAlias.getChildren().size() > 0) {
legendReplacement.getChildren().setAll(legendAlias.getChildren());
legendAlias.getChildren().clear();
setLegend(legendReplacement);
}
}
}
Is there a way to add listener whenever I resize my splitpane?
I currently have
split.getDividers().get(0).positionProperty().addListener(new ChangeListener<Number>(){
public void changed(ObservableValue<? extends Number> observableValue, Number oldWindowWidth, Number newWindowWidth){
//code
}
});
which detects whenever the window or divider size is changed, but I only need to know when the divider position is changed, like when I drag it with the mouse. Is there a way to do that? Any help will be appreciated!
There is no clean way to do this but it is possible for example by using CSS lookup on the SplitPane.
As the dividers have the CSS class of split-pane-divider you can get the dividers from the scene-graph, and they are actually StackPane instances.
On these StackPane you can register a mouse pressed and a mouse release event listener, and update a class member that indicates that the divider is "in a drag" at the moment. And then in the position property listener you can check this class member: if it is true that means that the divider is being moved by the mouse, otherwise the change can be ignored.
Example:
public class Main extends Application {
// Indicates that the divider is currently dragged by the mouse
private boolean mouseDragOnDivider = false;
#Override
public void start(Stage primaryStage) throws Exception{
SplitPane sp = new SplitPane();
sp.getItems().addAll(new StackPane(), new StackPane());
sp.setDividerPositions(0.3f);
// Listen to the position property
sp.getDividers().get(0).positionProperty().addListener((obs, oldVal, newVal) -> {
if(mouseDragOnDivider)
System.out.println("It's a mouse drag to pos: " + newVal.doubleValue());
});
primaryStage.setScene(new Scene(sp, 300, 275));
sp.requestLayout();
sp.applyCss();
// For each divider register a mouse pressed and a released listener
for(Node node: sp.lookupAll(".split-pane-divider")) {
node.setOnMousePressed(evMousePressed -> mouseDragOnDivider = true);
node.setOnMouseReleased(evMouseReleased -> mouseDragOnDivider = false );
}
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Note:
As the lookup only works if the layout is created and the CSS is applied, hence it is important that the requestLayout() and the applyCss() methods are already executed and also that the SplitPane is already added to the scene-graph (attached to a Scene).
I'm creating a custom header for my TableColumns that is the label of the column plus a TextField that will allow users to perform searches. I'm setting the column headers like so:
getColumns().addListener(new ListChangeListener<TableColumn<S, ?>>() {
#Override
public void onChanged(final ListChangeListener.Change<? extends TableColumn<S, ?>> change) {
while (change.next()) {
Label label;
TextField search;
VBox graphic;
for (TableColumn<S, ?> column : change.getAddedSubList()) {
label = new Label(column.getText());
search = new TextField();
graphic = new VBox();
graphic.getStyleClass().add("k-column-graphic");
graphic.getChildren().addAll(label, search);
column.setGraphic(graphic);
}
}
}
});
So the column's graphic is what is displayed. I'm using the following CSS (the graphic itself has a "k-column-graphic" CSS class, while the TableView has a "k-table-view" CSS class)
/** Hide default text label in KTableView */
.k-table-view .column-header > .label {
-fx-content-display: graphic-only;
}
.k-column-graphic {
-fx-alignment: center-left;
-fx-spacing: 5;
-fx-padding: 2;
}
This works great, but I'm also allowing the columns to be hidden by enabling the TableView.setTableMenuButtonVisible(true); property, which adds a button to easily hide columns.
Whenever I try to hide a column, it hides successfully, but the graphic (the Label/TextField) remain. Both seem to have a width of 0 or 1, and are very small, but you can still see them.
How, either through CSS or somewhere in my code, do I make it to where the graphic Node for the TableColumn will hide as well?
When you toggle the CheckMenuItem to show/hide the column, your customized controls won't automatically change their values of VisibleProperty. So what you need to do is simply bind the VisibleProperty of your own controls to the TableColumn's VisibleProperty.
Following sample is based on your code. Hoping it can help.
getColumns().addListener(new ListChangeListener<TableColumn<S, ?>>() {
#Override
public void onChanged(final ListChangeListener.Change<? extends TableColumn<S, ?>> change) {
while (change.next()) {
Label label;
TextField search;
VBox graphic;
for (TableColumn<S, ?> column : change.getAddedSubList()) {
label = new Label(column.getText());
search = new TextField();
graphic = new VBox();
graphic.getStyleClass().add("k-column-graphic");
graphic.getChildren().addAll(label, search);
column.setGraphic(graphic);
/* ======= add the following two lines ============== */
label.visibleProperty().bind(column.visibleProperty());
search.visibleProperty().bind(column.visibleProperty());
}
}
}
});
I have the following "tree" of objects:
JPanel
JScrollPane
JPanel
JPanel
JScrollPane
JTextPane
When using the mouse wheel to scroll over the outer JScrollPane I encounter one annoying problem. As soon as the mouse cursor touches the inner JScrollPane, it seems that the scrolling events get passed into that JScrollPane and are not processed anymore by the first one. That means that scrolling the "parent" JScrollPane stops.
Is it possible to disable only the mouse wheel on the inner JScrollPane? Or even better, disable scrolling if there is nothing to scroll (most of the time the textpane only contains 1-3 lines of text), but enable it if there is more content?
I have run into this annoying problem also, and Sbodd's solution was not acceptable for me because I needed to be able to scroll inside tables and JTextAreas. I wanted the behavior to be the same as a browser, where the mouse over a scrollable control will scroll that control until the control bottoms out, then continue to scroll the parent scrollpane, usually the scrollpane for the whole page.
This class will do just that. Just use it in place of a regular JScrollPane. I hope it helps you.
/**
* A JScrollPane that will bubble a mouse wheel scroll event to the parent
* JScrollPane if one exists when this scrollpane either tops out or bottoms out.
*/
public class PDControlScrollPane extends JScrollPane {
public PDControlScrollPane() {
super();
addMouseWheelListener(new PDMouseWheelListener());
}
class PDMouseWheelListener implements MouseWheelListener {
private JScrollBar bar;
private int previousValue = 0;
private JScrollPane parentScrollPane;
private JScrollPane getParentScrollPane() {
if (parentScrollPane == null) {
Component parent = getParent();
while (!(parent instanceof JScrollPane) && parent != null) {
parent = parent.getParent();
}
parentScrollPane = (JScrollPane)parent;
}
return parentScrollPane;
}
public PDMouseWheelListener() {
bar = PDControlScrollPane.this.getVerticalScrollBar();
}
public void mouseWheelMoved(MouseWheelEvent e) {
JScrollPane parent = getParentScrollPane();
if (parent != null) {
/*
* Only dispatch if we have reached top/bottom on previous scroll
*/
if (e.getWheelRotation() < 0) {
if (bar.getValue() == 0 && previousValue == 0) {
parent.dispatchEvent(cloneEvent(e));
}
} else {
if (bar.getValue() == getMax() && previousValue == getMax()) {
parent.dispatchEvent(cloneEvent(e));
}
}
previousValue = bar.getValue();
}
/*
* If parent scrollpane doesn't exist, remove this as a listener.
* We have to defer this till now (vs doing it in constructor)
* because in the constructor this item has no parent yet.
*/
else {
PDControlScrollPane.this.removeMouseWheelListener(this);
}
}
private int getMax() {
return bar.getMaximum() - bar.getVisibleAmount();
}
private MouseWheelEvent cloneEvent(MouseWheelEvent e) {
return new MouseWheelEvent(getParentScrollPane(), e.getID(), e
.getWhen(), e.getModifiers(), 1, 1, e
.getClickCount(), false, e.getScrollType(), e
.getScrollAmount(), e.getWheelRotation());
}
}
}
Inspired by the existing answers, I
took the code from Nemi's answer
combined it with kleopatra's answer to a similar question to avoid constructing the MouseWheelEvent verbosely
extracted the listener into its own top-level class so that it can be used in contexts where the JScrollPane class cannot be extended
inlined the code as far as possible.
The result is this piece of code:
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
/**
* Passes mouse wheel events to the parent component if this component
* cannot scroll further in the given direction.
* <p>
* This behavior is a little better than Swing's default behavior but
* still worse than the behavior of Google Chrome, which remembers the
* currently scrolling component and sticks to it until a timeout happens.
*
* #see Stack Overflow
*/
public final class MouseWheelScrollListener implements MouseWheelListener {
private final JScrollPane pane;
private int previousValue;
public MouseWheelScrollListener(JScrollPane pane) {
this.pane = pane;
previousValue = pane.getVerticalScrollBar().getValue();
}
public void mouseWheelMoved(MouseWheelEvent e) {
Component parent = pane.getParent();
while (!(parent instanceof JScrollPane)) {
if (parent == null) {
return;
}
parent = parent.getParent();
}
JScrollBar bar = pane.getVerticalScrollBar();
int limit = e.getWheelRotation() < 0 ? 0 : bar.getMaximum() - bar.getVisibleAmount();
if (previousValue == limit && bar.getValue() == limit) {
parent.dispatchEvent(SwingUtilities.convertMouseEvent(pane, e, parent));
}
previousValue = bar.getValue();
}
}
It is used like this:
JScrollPane pane = new JScrollPane();
pane.addMouseWheelListener(new MouseWheelScrollListener(pane));
Once an instance of this class is created and bound to a scroll pane, it cannot be reused for another component since it remembers the previous position of the vertical scroll bar.
Sadly, the obvious solution (JScrollPane.setWheelScrollingEnabled(false)) doesn't actually deregister for MouseWheelEvents, so it doesn't achieve the effect you want.
Here's a crude-hackery way of disabling scrolling altogether that will let the MouseWheelEvents reach the outer JScrollPane:
for (MouseWheelListener mwl : scrollPane.getMouseWheelListeners()) {
scrollPane.removeMouseWheelListener(mwl);
}
If you do this to your inner JScrollPane, it'll never respond to scroll wheel events; the outer JScrollPane will get all of them.
If you want to do it "cleanly", you'd need to implement your own ScrollPaneUI, and set that as the JScrollPane's UI with setUI(). Unfortunately, you can't just extend BasicScrollPaneUI and disable its mouse wheel listener, because the relevant member variables are private and there aren't any flags or guards on the ScrollPaneUI's installation of its MouseWheelListener.
For your "even better" solution, you'd have to dig deeper than I have time to into the ScrollPaneUI, find the hooks where the scrollbars get made visible / invisible, and add/remove your MouseWheelListener at those points.
Hope that helps!
#Nemi has a good solution already.
I boiled it down a bit further, putting the follwing method in my library:
static public void passMouseWheelEventsToParent(final Component pComponent, final Component pParent) {
pComponent.addMouseWheelListener((final MouseWheelEvent pE) -> {
pParent.dispatchEvent(new MouseWheelEvent(pParent, pE.getID(), pE.getWhen(), pE.getModifiers(), 1, 1, pE.getClickCount(), false, pE.getScrollType(), pE.getScrollAmount(), pE.getWheelRotation()));
});
}