Scaling a button's text automatically with the button in JavaFX - java

I made a grid of buttons in JavaFX.
When I resize the window with the grid inside, the buttons resize within the grid as well.
The problem is that the text on those buttons doesn't resize along with them: it stays the same size all the time, so when the buttons grow big, there's a lot of empty space on a button and then a tiny little text in the middle, which looks terrible.
I would rather like the text to automatically resize along with these buttons to fit the empty space, so that when the entire user interface gets bigger, the text on the buttons gets bigger as well.
How can I accomplish that?
I tried setting the -fx-font-size in the CSS stylesheet to percentage values, but it doesn't seem to work the same way as for websites: the text doesn't scale as a percentage of its container, but as a percentage of some predefined text size.
Edit
This is not a duplicate! Stop marking each question out there as duplicate! If it has been answered, I wouldn't have asked it in the first place!
From what I see, the first of those threads was about a situation where someone wanted to set the size/style of the text for newly-created buttons to account for the current size of their container etc. This is not what I need, because I want the buttons which has been already created as well to automatically resize their texts when these buttons resize inside their container in some way.
The other thread was about scaling the text along with the root container / window with a preset font size. This is also different from what I need, because I don't want the text to be scaled with the window, but with the sizes of the buttons themselves. And it has to be scaled in a certain way: to always fit the size of the button. You know: the text stays the same, but stretches so that it always fits the inside of the button (with a little padding, not a huge empty area around the text).
It is the button's size which is to determine the size of the text on it, not the window or container or something else, and it needs to be done automatically by the button itself (either the built-in one or a subclassed one), not manually by its encompassing container iterating over all these buttons and updating their text's sizes (which would be dumb way to do it).

This is, liked the linked questions, something of a hack: but consider scaling the text node inside the button instead of changing the font size. This seems to work ok:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
public class ScaledButtons extends Application {
#Override
public void start(Stage primaryStage) {
GridPane root = new GridPane();
root.setHgap(5);
root.setVgap(5);
for (int i = 1; i <= 9 ; i++) {
root.add(createScaledButton(Integer.toString(i)), (i-1) % 3, (i-1) / 3);
}
root.add(createScaledButton("#"), 0, 3);
root.add(createScaledButton("0"), 1, 3);
root.add(createScaledButton("*"), 2, 3);
primaryStage.setScene(new Scene(root, 250, 400));
primaryStage.show();
}
private Button createScaledButton(String text) {
Button button = new Button(text);
GridPane.setFillHeight(button, true);
GridPane.setFillWidth(button, true);
GridPane.setHgrow(button, Priority.ALWAYS);
GridPane.setVgrow(button, Priority.ALWAYS);
button.layoutBoundsProperty().addListener((obs, oldBounds, newBounds) ->
scaleButton(button));
button.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
return button ;
}
private void scaleButton(Button button) {
double w = button.getWidth();
double h = button.getHeight();
double bw = button.prefWidth(-1);
double bh = button.prefHeight(-1);
if (w == 0 || h == 0 || bw == 0 || bh == 0) return ;
double hScale = w / bw ;
double vScale = h / bw ;
double scale = Math.min(hScale, vScale);
button.lookup(".text").setScaleX(scale);
button.lookup(".text").setScaleY(scale);
}
public static void main(String[] args) {
launch(args);
}
}

An alternate approach to get a similar effect could be to subclass com.sun.javafx.scene.control.skin.ButtonSkin and override the layoutLabelInArea(double x, double y, double w, double h, Pos alignment) method from the skin's parent (LabeledSkinBase). You can then explicitly assign the updated skin to your button (either via CSS or via Java API calls).
Doing so would requires the subclassing of com.sun APIs which could change without notice in subsequent JavaFX releases. Also layoutLabelInArea is reasonably complex in its operation so changing the layout logic could be a little tricky. Certainly, James's suggestion of applying a text rescaling operation based upon a listener to the layout bounds property is simpler in this particular case.
I'm not necessarily advocating this approach, just providing a route to something that you could create that would satisfy your goal of: "It is the button's size which is to determine the size of the text on it, not the window or container or something else, and it needs to be done automatically by the button itself".

Related

Make scrollpane always on top

So, in my program, at the bottom, I've got a ScrollPane which has a TextFlow inside it to display a information in when the user clicks on items in a list. The ScrollPane was rather small, so I added an expand button which makes it larger so you can see more. But now, if there is a button or anything within the area that the expanded ScrollPane takes up, it is always on top of the pane and makes it hard to see anything. Is there a way I could force the ScrollPane to be on top and cover things? Attempting to change the z-index did nothing.
Should I just set anything within that area to be not visible while expanded instead?
This is what I'm doing to expand the ScrollPane:
Image expand_img = new Image(getClass().getResourceAsStream("expand.png"));
Button expand = new Button();
expand.setGraphic(new ImageView(expand_img));
expand.setLayoutX(630);
expand.setLayoutY(415);
expand.setStyle("-fx-background-color: transparent");
expand.addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent e) {
++clicked;
if (clicked % 2 == 0){
scroll_desc.setTranslateY(-100);
scroll_desc.setPrefHeight(208);
expand.setLayoutY(315);
}else if (clicked % 2 == 1){
scroll_desc.setTranslateY(0);
scroll_desc.setPrefHeight(108);
expand.setLayoutY(415);
}
}
});
I just want it so that when the ScrollPane is larger, anything inside the new area is covered up by the ScrollPane rather than being overlayed on top and accessible, thus obscuring content of the descriptions and the like displayed.

ListView with custom CellFactory trims invisible nodes

My layout issue
I have a little issue with ListView and I'm not sure if it's because of some knowledge I missing or if my approach is flawed. Have to admit I'm not yet clear with how JavaFX handle the layout in the many possible cases.
The above screenshot shows the result I get twice with the exact same code, except that on the second one an invisible shape I use for coherent layout is made visible for debug.
The various classes involved by the CellFactory extend Group, I tried with some other Parent without much success so far.
How to reproduce
Rather than sharing my StarShape, StarRow and some other misc classes (I'd be happy to if requested) I wrote a sample reproducing the issue. The class extends Application and overrides the start(...) method as such:
#Override
public void start(Stage primaryStage) throws Exception {
final StackPane root = new StackPane();
final Scene scene = new Scene(root, 400, 600);
final ListView<Boolean> listView = new ListView<>();
listView.setCellFactory(this::cellFactory);
for (int i = 0; i < 5 ; i++) {
listView.getItems().add(true);
listView.getItems().add(false);
}
root.getChildren().add(listView);
primaryStage.setScene(scene);
primaryStage.setTitle("ListView trims the invisible");
primaryStage.show();
}
where this::cellFactory is
private ListCell<Boolean> cellFactory(ListView<Boolean> listView) {
return new ListCell<Boolean>() {
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
label.setLayoutX(40);
final Group cellRoot = new Group();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
};
}
The above will display a ListView<Boolean> with black shapes in front of true items (because of the tabShape.setVisible(item); bit). The false items are looking like regular Label objects as if the invisible shape in their Group wasn't there (but it is).
Closing comments
Debugging this, it turns out groups with the invisible shapes are given negative layoutX property values. Thus Label controls aren't aligned as I'd like them to be. It doesn't happen when I call setLayoutX and setLayoutY outside of a ListView (the invisible shapes do force offsets), but it's probably not the only place where it would happen.
What's happening and how to avoid it? Alternatively, as I'm guessing I'm approaching this wrong, what'd be the right way? In other words, what is the question I should be asking instead of this?
Taking from #dlatikay's comment, instead of setting the placeholder items to invisible, you can render them transparent by setting their opacity to 0.0.
Applied to the MCVE from your question, this would be done by replacing:
tabShape.setVisible(item);
with:
tabShape.setOpacity(item ? 1.0 : 0.0);
In terms of user experience, you could take this one step further. Instead of setting the "inactive" stars to fully transparent, you could set them to be near-transparent, as in this mockup (with opacity set to 0.1):
The benefits that I see are:
It indicates not only the rating of an item in the list, but also the maximum rating.
It avoids awkward empty spaces for list items with zero stars.
I'm guessing I'm approaching this wrong
No, you're not. As with all layouts, there's often multiple ways to approach the same problem. Your approach is actually correct, and you're very close to a working solution.
You can achieve what you're after with a mere 1 line change. That is, changing the Group to an HBox.
An HBox ensures that elements are ordered horizontally, one after another. They also allow invisible elements to still take up space.
I also commented out one line: label.setLayoutX(40). I did this because HBox will not respect this setting, and actually you don't need it to. It will automatically shift the elements horizontally by as much is required.
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
}
else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
//label.setLayoutX(40);
final HBox cellRoot = new HBox();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
When I make those changes, your layout will render like so:
Important: Your example and your screenshots are slightly different. You may want to use a VBox for your star example (V for 'vertical', H for 'horizontal').

JavaFX TextFlow - empty Text Node not displaying empty (stealing character from adjacent Text)

This is how the window looks
The issue here is that the center Text (blue) should be empty, but for some reason is not.
The window is just a very simple JavaFX Application. The start method looks like this:
public void start(Stage stage) {
Pane main = new Pane();
TextProgress copy = new TextProgress();
main.getChildren().add(copy);
Scene scene = new Scene(main, 300, 200);
stage.setTitle("Test");
stage.setScene(scene);
stage.show();
}
TextProgress is a custom class that is basically a TextFlow with three Text Nodes in it. The contents of the three Texts is: "Hello ", "" (empty), "World". The problem is, as you can see in the image, that the Text in the middle 'stole' a character from the one on the right.
Before showing the code of TextProgress, let me list a few things that I've found while playing around with this, as to make the code understandable.
This entire code is just to show the problem, the original application was basically something to display the progress of copying a text by hand. The left Text would contain all the words that were already typed, the center Text the current word to type, and the right Text the text yet to type.
Furthermore, the center Text was split once more into a Text for the typed and untyped characters for the current word. In this example the "Hello" would be an already typed word, the empty Text would be the typed characters of the current word, and "World" would be the untyped characters of the current word. (Since we are not typing anything in this example, the other Text was left out.)
The reason I did not leave out the "Hello" is that the issue only occurs when there is a Text in front of the center Text (doesn't matter if empty or not).
Another way to prevent the issue is wrapping the two Texts for the current word in another TextFlow. (Text, TextFlow[Text,Text], Text). While this seems to solve the issue, it causes a different one. While typing text, and thus changing the contents of the Texts, the word wrap of the line currently being typed changes from time to time, e.g. a long word gets pushed down into the next line while being typed, then goes back up on the next word again. Note that this happens even though the combined length of all Texts always stays the same. This might be related to the issue of this question?
The only difference between the different Texts is the color. Regarding the other issue mentioned above, does this explain the changing word wrap?
The issue only ever occurred on the first character of the current word. Once the center Text (blue) is not empty anymore, everything is as expected.
I made sure the empty Text is actually empty
I added some prints for the BoundingBox of the center Text before and after displaying the window, the width attribute here is 0 before, ~28 afterwards.
If the two Texts for the current word are wrapped in another TextFlow, the width attribute here changes from 0 to -1. Is there a way to set the width of the empty Text to -1 without the additional TextFlow?
(Not sure whether to include just the TextProgress class here, or the entire Application, since the entire class is small.)
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class Window extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Pane main = new Pane();
TextProgress copy = new TextProgress();
main.getChildren().add(copy);
Scene scene = new Scene(main, 300, 200);
stage.setTitle("Test");
stage.setScene(scene);
stage.show();
}
private static class TextProgress extends TextFlow {
private Text typed;
private TextFlow current;
private Text currentTyped;
private Text currentUntyped;
public TextProgress()
{
double size = 30;
boolean wrap = false;
typed = new Text();
typed.setFont(Font.font(size));
typed.setFill(Color.ORANGERED);
typed.setText("Hello ");
currentTyped = new Text();
currentTyped.setFont(Font.font(size));
currentTyped.setFill(Color.DEEPSKYBLUE);
currentTyped.setText("");
printDelayed(1000);
currentUntyped = new Text();
currentUntyped.setFont(Font.font(size));
currentUntyped.setFill(Color.MEDIUMAQUAMARINE);
currentUntyped.setText("World");
if (wrap) {
current = new TextFlow();
current.getChildren().addAll(currentTyped, currentUntyped);
this.getChildren().addAll(typed, current);
} else {
this.getChildren().addAll(typed, currentTyped, currentUntyped);
}
}
private void printDelayed(long delay) {
System.out.println(currentTyped.getLayoutBounds());
new Thread(() -> {
try {
Thread.sleep(delay);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(currentTyped.getLayoutBounds());
}).start();
}
}
}
I appreciate any clues as to what is going on here. Again, the main question here is how to stop the center Text from displaying some character from the adjacent Text.
This is what i am guessing you are doing, which should work from the way my head is vibrating now :)
You have a TextFlow which has 3 Text (or Label) Nodes, respectively typed, untyped,currenttyped, all with the initial text " ".
Now when you type the key strokes are updated in the typed and currenttyped, and you conjure the next set of words and update it to untyped.
so it should work, because Textflow is just a horizontal,wrappable node of nodes, so if you have Labels or Text with different Paints all you have to do is update those respective nodes textProperty(), and i guess they will wrap on their own.
Hope it helped you Sir.

How can I scale font size to automatically accommodate a static JTextField size?

I'm making a Matrix calculator in Java using a two-dimensional array of JTextFields on a JPanel to input a given Matrix. Since the panel that this matrix is placed on has a fixed size, I want to scale down the text size to accommodate how long the number is as it's being typed in.
For example:
If it's one digit long, font size = 18. If it's two digits long, font size = 14. Etc.
I want this to dynamically occur as the user is entering the text, and I want repainting to occur everytime the user interacts with the JTextField, not only after the user presses "enter". This is because the field is transparent and currently looks sloppy (numbers overlapping) until the user presses enter.
What's the most straightforward and reliable way to do this? Here's my current code if it helps anyone answer my question:
public class MatrixPanel extends JPanel implements ActionListener
{
float[][] matrice = new float[3][3];
JTextField[][] parameter = new JTextField[3][3];
Font font = new Font("SansSerif", Font.BOLD, 40);
public MatrixPanel(String title)
{
setLayout(null);
setOpaque(false);
for (int width = 0; width < 3; width++){
for (int height = 0; height < 3; height++){
matrice[width][height] = 0;
parameter[width][height] = new JTextField();
parameter[width][height].setHorizontalAlignment(JTextField.CENTER);
parameter[width][height].setFont(font);
parameter[width][height].setText("0");
parameter[width][height].setLocation((50*width), (50*height));
parameter[width][height].setSize(50,50);
parameter[width][height].setOpaque(false);
parameter[width][height].setBorder(null);
parameter[width][height].addActionListener(this);
add(parameter[width][height]);
}
setSize(150,150);
}
}
I want to scale down the text size to accommodate how long the number
is as it's being typed in.
Take a look to How to Write a DocumentListener to achieve your goal.
Some useful tips:
About setSize(150,150) and setLocation((50*width), (50*height)): please have a look to this topic Should I avoid the use of set(Preferred|Maximum|Minimum)Size methods in Java Swing? (yes we should). Components size and location should be handled by layout managers.
You may want to try GridLayout to place the text fields in a grid and forget about the fixed size/location. See How to Use GridLayout and Laying Out Components Within a Container tutorials for a better understanding on this subject.
If you want to input floats (or doubles) in your matrix then maybe formatted text fields or spinners are a better choice than plain text fields. Take a look to How to Use Formatted Text Fields and How to Use Spinners tutorials.

No vertical scroll in browser

I'm developing a Vaadin application and am having extreme difficulty getting some aspects of the layout as I want. The major problem right now is that I can't seem to get a vertical scroll in my layout no matter how big the size of the content is or how small the browser window is..
I have read up on the subject, I know that the hLayout and the vLayout doesn't support scrollbars but the Panel do. I've tried in many different combinations to make it work but I've only managed to get a horizontal scrollbar to generate but never a vertical one.
Another problem is that I'm building the application inside an existing "template" provided by the company. This template contains a footer containing some copyright information. This footer doesn't seem to occupy any space in the browser window with regards to the content I'm adding, which causes when viewing on smaller screens the horizontal scrollbar to appear "underneath" the footer, non-accessible... I'll provide some of the code of how it looks now.
public class InventorySimCardTable extends M2MViewBase { //M2MViewBase extends VerticalLayout
private final SPanel mainContent = Cf.panel("");
private final SPanel tabPanel = Cf.panel("");
private final SVerticalLayout tabcontent = Cf.vLayout();
protected InventoryFilterPanel inventoryFilterPanel;
#Override
protected void initComponent() {
setSizeFull();
tabPanel.setSizeFull();
tabPanel.getContent().setSizeUndefined();
Table simCardTable = new Table();
simCardTable.setWidth("1898px");
simCardTable.setPageLength(15);
tableContainer.setSizeUndefined();
tableContainer.addComponent(simCardTable);
mainContent.setWidth("99%");
mainContent.setHeight("100%");
mainContent.setContent(tableContainer);
mainContent.setScrollable(true);
centeringlayout.setSizeFull();
centeringlayout.addComponent(mainContent);
centeringlayout.setComponentAlignment(mainContent, Alignment.MIDDLE_CENTER);
tabPanel.addComponent(centeringlayout);
addComponent(tabPanel);
}
}
I would love to know if anyone sees any obvious errors in my code. And if anyone knows what property I can set on the footer CSS to have it occupy space in the content view so that the horizontal scroll doesn't appear underneath it. Thank you!
What I did to solve this issue was to structure the code as follows. This will create a vertical and horizontal scroll bar for the Panel holding my filter component and the table. Hopefully this can help someone with a similar problem.
#Override
protected void initComponent() {
super.initComponent();
if(!tableCreated) {
createSimCardsTable();
tableCreated = true;
}
mainWindow = this.getWindow();
Panel basePanel = new Panel("");
basePanel.addComponent(inventoryFilterPanel);
AbstractComponent separatorLine = Cf.horizontalLine(); //Of no signficance
separatorLine.addStyleName("m2m-horizontal-line-list-separator");
separatorLine.setWidth("99%");
basePanel.addComponent(separatorLine);
basePanel.addComponent(simCardTable);
basePanel.setSizeFull();
basePanel.getContent().setSizeUndefined(); // <-- This is the important part
addComponent(basePanel);
setExpandRatio(basePanel, 1);
}
All Vaadin components have size undefined by default, so usually there is no need to call method setSizeUndefined(). Also there is no need to call setScrollable(true), because it enables only programmatic scrolling possibility.
When I was trying to make a sense of scrolling appearance I wrote a simple skeleton of layout. Try this out as a content of the main window:
import com.vaadin.ui.HorizontalSplitPanel;
import com.vaadin.ui.Label;
import com.vaadin.ui.Panel;
import com.vaadin.ui.VerticalLayout;
public class Skeleton extends VerticalLayout {
public Skeleton() {
setSizeFull();
addComponent(new Label("Header component"));
HorizontalSplitPanel splitPanel = new HorizontalSplitPanel();
Panel leftComponent = new Panel();
Panel rightComponent = new Panel();
splitPanel.setFirstComponent(leftComponent);
splitPanel.setSecondComponent(rightComponent);
for (int i = 0 ; i < 200 ; i ++) {
leftComponent.addComponent(new Label("left"));
rightComponent.addComponent(new Label("right"));
}
leftComponent.setSizeFull();
rightComponent.setSizeFull();
addComponent(splitPanel);
setExpandRatio(splitPanel, 1);
addComponent(new Label("Footer component"));
}
}
You should see scrollbars inside the nested panels. But if setSizeFull() is removed from Skeleton layout, then it is not limited in size (by default) and grows downwards - then only the scrollbar of the whole window appears.
Add this to your styles.css
.v-verticallayout > div {
overflow-y: auto ! important;
}
First of all try to make your panel scrollable by calling setScrollable(true) method, but this will not work if you set some custom layout with setSizeFull() as this panel new layout.
If you exactly know that you application will be opened in device with small screen resolution, you simple can set for your "primary"/"main" layout some fixed width and height, or add some CSS style with params like min-width: {some value} px, min-height: {some value} px.
Based on this post, I added vertical.setSizeUndefined(); and started seeing vertical scrollbars.
setMainWindow(new Window(title ));
vertical.setSizeFull();
vertical.setHeight("100%");
toolbar = createToolbar();
vertical.addComponent(toolbar);
vertical.setExpandRatio(toolbar, 0.03f);
Component tree = buildTree();
vertical.addComponent(tree);
vertical.setExpandRatio(tree, 0.97f);
vertical.setSizeUndefined();
getMainWindow().setContent(vertical);>
The only way I could fix this now (v6.8.14) is to specify the height in px values in stead of %
Use CustomLayout, always. It's faster, more efficient and by controlling html and css easily you can acieve a graphically consistent result

Categories

Resources