1

I have a tilepane inside a scrollpane. The tilepane is filled with buttons that have actions like delete or edit. After I delete or edit a button, the scrollpane automatically goes back to the top, which I don't want. I want it to remain at the current position (the position the user scrolled to).

I tried getting and setting the vValue of the scrollpane. getvValue gets the value and the setter sets it, but the scrollpane doesn't respond to it and goes back to the top after the action (delete/ edit). I tried a solution from this question: JavaFX ScrollPane setVvalue() not working as intended, but the layout() method doesn't do anything either. What am I doing wrong here? How do I make the scrollpane stay where it is?

EDIT: I found the problem, but still don't know how to fix it. Apparently setting and changing the visibility on the hBoxes in fxml puts the scrollpane back to the top. If I remove the setVisible methods and run it, I can delete items and the scrollpane stays in position. How to fix this?

On request I created a Minimal, Complete, and Verifiable example of my code. Run it, scroll down a bit, right click a button with a name and price on it and then click delete. It scrolls back up to the top.

This is my code:

public class Controller {
private static LinkedHashMap<String, BigDecimal> mProductMap = new LinkedHashMap<>();

@FXML private TilePane fieldContainer = new TilePane();
@FXML private ScrollPane scroll;
@FXML private Button deletebtn;
@FXML private Button editbtn;
@FXML private HBox homeBar;
@FXML private HBox actionBar;

@FXML
private void initialize() {
    addProduct("Coffee", new BigDecimal("2.00"));
    addProduct("Tea", new BigDecimal("2.00"));
    addProduct("Cappuccino", new BigDecimal("2.00"));
    addProduct("Espresso", new BigDecimal("2.00"));
    addProduct("Cooky", new BigDecimal("2.00"));
    addProduct("Candy", new BigDecimal("2.00"));
    addProduct("Chocobar", new BigDecimal("2.00"));
    addProduct("Cola", new BigDecimal("2.00"));
    addProduct("Fanta", new BigDecimal("2.00"));
    addProduct("Beer", new BigDecimal("2.00"));
    addProduct("Salad", new BigDecimal("2.00"));
    addProduct("Sandwich", new BigDecimal("2.00"));
    addProduct("Water", new BigDecimal("2.00"));
    addProduct("Cassis", new BigDecimal("2.00"));
}

// makes the delete and edit buttons appear after selecting a button from the tilepane
private void select(String selectedProduct) {

    /*
    the setVisible method for the hBoxes in fxml cause the scrollbar to go back to the top
    without them, the scrollpane stays where it is.
    I tried changing visibility with both setVisible and CSS, but they both cause the problem
    I need the actionbar to appear when you select a button (right click on it)
    */
    homeBar.setVisible(false);
    actionBar.setVisible(true);

    EventHandler<ActionEvent> delete = event -> {
        deleteProduct(selectedProduct);  // deletes an item from a LinkedHashMap
        homeBar.setVisible(true);
        actionBar.setVisible(false);
    };
    deletebtn.setOnAction(delete);

    // I want the same to happen when the edit handler is used, scrollpane needs to remain its position
    EventHandler<ActionEvent> edit = event -> {
        editProduct(selectedProduct);  // edits an item from a LinkedHashMap
        homeBar.setVisible(true);
        actionBar.setVisible(false);
    };
    editbtn.setOnAction(edit);
}

/*
Code below does not cause the problem, but I added it as a reference
*/

private void deleteProduct(String product) {
    if (mProductMap.containsKey(product)) {
        mProductMap.remove(product);
        System.out.printf("%s has been deleted!%n", product);
    } else {
        System.out.printf("%s does not exist. Please try again.%n", product);
    }
    addButtons();
}

private void editProduct(String product) {
    List<String> indexKeys = new ArrayList<>(mProductMap.keySet());
    List<BigDecimal> indexValues = new ArrayList<>(mProductMap.values());
    BigDecimal price = mProductMap.get(product); // gets the product's value (the price)
    int indexKey = indexKeys.indexOf(product);
    int indexValue = indexValues.indexOf(price);

    if (mProductMap.containsKey(product)) {
        int sizeBefore = mProductMap.size();
        addingProduct();
        int sizeAfter = mProductMap.size();

        if (sizeAfter > sizeBefore) {
            indexKeys.remove(product);
            indexValues.remove(price);
            mProductMap.remove(product);

            // Make a new list to get the new entry at the end
            List<Map.Entry<String,BigDecimal>> entryList = new ArrayList<>(mProductMap.entrySet());
            Map.Entry<String, BigDecimal> lastEntry = entryList.get(entryList.size()-1);

            String key = lastEntry.getKey();
            BigDecimal value = lastEntry.getValue();
            indexKeys.add(indexKey, key);
            indexValues.add(indexValue, value);
            mProductMap.clear();

            // Put the keys and values from the two lists back to the map
            for (int i=0; i<indexKeys.size(); i++) {
                addProduct(indexKeys.get(i), indexValues.get(i));
            }
        }
    } else {
        System.out.printf("%s does not exist. Please try again.%n", product);
    }
}

void addProduct(String product, BigDecimal price) {
    mProductMap.put(product, price);
    addButtons();
}

// Adding buttons to the TilePane fieldContainer in center of BorderPane
// One button per key-value pair of mProductMap
private void addButtons() {
    // clears the TilePane to prevent duplicate buttons
    fieldContainer.getChildren().clear();

    for (Map.Entry<String, BigDecimal> entry : mProductMap.entrySet()) {
        StackPane newField = new StackPane();
        Button main = new Button();
        main.setOnMousePressed(me -> {
            if (me.getButton() == MouseButton.SECONDARY) {  // = right click
                select(entry.getKey());
            }
        });
        main.setText(entry.getKey() + "\n" + entry.getValue());
        newField.getChildren().add(main);
        fieldContainer.setAlignment(Pos.TOP_LEFT);
        fieldContainer.getChildren().add(newField);
    }
}

// Popup for adding products to the Map with the + button
@FXML
private void addingProduct(){
    Stage newStage = new Stage();
    VBox popup = new VBox();
    final BooleanProperty firstTime = new SimpleBooleanProperty(true); // Variable to store the focus on stage load
    TextField product = new TextField("");
    product.setId("product");
    product.setPromptText("Enter the item name...");
    // code to remove the focus from first textfield on stage load
    product.focusedProperty().addListener((observable,  oldValue,  newValue) -> {
        if(newValue && firstTime.get()){
            popup.requestFocus(); // Delegate the focus to container
            firstTime.setValue(false); // Variable value changed for future references
        }
    });

    TextField price = new TextField("");
    price.setId("price");
    price.setPromptText("Enter the item price...");
    Button submit = new Button("Submit");
    Label label = new Label();
    label.setId("label");
    submit.setOnAction(e -> {
        if ( (product.getText() != null && !product.getText().isEmpty() &&
                price.getText() != null && !price.getText().isEmpty() ) ) {
            addProduct(product.getText(), new BigDecimal(price.getText()) );
            newStage.close();
        } else {
            label.setText("Fill in both fields");
        }
    });

    popup.getChildren().add(product);
    popup.getChildren().add(price);
    popup.getChildren().add(submit);
    popup.getChildren().add(label);
    Scene stageScene = new Scene(popup, 300, 200);
    newStage.setScene(stageScene);
    newStage.showAndWait();
  }
}

The FXML:

<BorderPane xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
<top>
    <StackPane>
        <HBox fx:id="homeBar" styleClass="main-bar" visible="true">
            <Button StackPane.alignment="BOTTOM_LEFT">Home</Button>
            <Button onAction="#addingProduct" StackPane.alignment="BOTTOM_RIGHT">Add a new product</Button>
        </HBox>
        <HBox fx:id="actionBar" styleClass="main-bar" visible="false">
            <Button fx:id="deletebtn" StackPane.alignment="BOTTOM_CENTER">Delete</Button>
            <Button fx:id="editbtn" StackPane.alignment="BOTTOM_CENTER">Edit</Button>
            <Button onAction="#addingProduct" StackPane.alignment="BOTTOM_RIGHT">Add a new product</Button>
        </HBox>
    </StackPane>
</top>

<center>
    <ScrollPane fx:id="scroll" hbarPolicy="NEVER">
        <TilePane fx:id="fieldContainer" prefColumns="2" prefTileHeight="100.0" prefTileWidth="144.0">
        </TilePane>
    </ScrollPane>
</center>

<bottom>
</bottom>
</BorderPane>

Main:

public class Main extends Application {

@Override
public void start(Stage primaryStage) throws Exception{
    Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
    primaryStage.setScene(new Scene(root, 300, 275));
    primaryStage.show();
}
public static void main(String[] args) {
    launch(args);
  }
}
Isoldhe
  • 300
  • 1
  • 7
  • 20
  • 1
    This shouldn't happen, unless for some reason you are rebuilding the entire `TilePane` when you delete one item. Create a [MCVE]. – James_D Dec 15 '17 at 12:50
  • You clear `TilePane` every time an item is removed, hence the behavior. – DVarga Dec 15 '17 at 14:49
  • I know, it's to prevent duplicate buttons. But if I comment out fieldContainer.getChildren().clear(); from the addButtons method, the problem doesn't resolve. It still scrolls back to the top. – Isoldhe Dec 15 '17 at 15:05
  • So I tried commenting out the clearing of the TilePane. Then I tried putting it in its own method, and also on the delete method commenting out the addButtons method (which adds one button for each item in the LinkedHashMap), so the button remains there after deleting, just to see if clearing or reloading the map for the buttons causes it to scroll back to the top, but it doesn't. After all these tries, it still scrolls back. Unless I'm missing something? – Isoldhe Dec 15 '17 at 15:30

1 Answers1

1

I fixed it myself, with help from answers from this post: ScrollPane jumps to top when deleting nodes.

After deleting a button from the TilePane, the ScrollPane finds the next node to focus on, which by default is the first node from the pane. By requesting the focus on the TilePane (fieldContainer) the ScrollPane stays in place.

I added this code to both the delete and edit methods:

fieldContainer.requestFocus();
Isoldhe
  • 300
  • 1
  • 7
  • 20