I use an extended class of BarChart to add vertical lines and text on top of bars. This works fine but I want to display a small text at the top right of the horizontal line.
I tried this without success:
public void addHorizontalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (horizontalMarkers.contains(marker)) return;
Line line = new Line();
line.setStroke(Color.RED);
marker.setNode(line);
getPlotChildren().add(line);
// Adding a label
Node text = new Text("average");
nodeMap.put(marker.getNode(), text);
getPlotChildren().add(text);
horizontalMarkers.add(marker);
}
#Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
for (Node bar : nodeMap.keySet()) {
Node text = nodeMap.get(bar);
text.relocate(bar.getBoundsInParent().getMinX() + bar.getBoundsInParent().getWidth()/2 - text.prefWidth(-1) / 2, bar.getBoundsInParent().getMinY() - 30);
}
for (Data<X, Y> horizontalMarker : horizontalMarkers) {
Line line = (Line) horizontalMarker.getNode();
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
line.setEndY(line.getStartY());
line.toFront();
}
}
What I'm doing wrong ?
You need to move the Text after you move the marker, i. e. your code integrated at the required position:
for (Data<X, Y> horizontalMarker : horizontalMarkers) {
Line line = (Line) horizontalMarker.getNode();
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
line.setEndY(line.getStartY());
line.toFront();
Node text = nodeMap.get(line);
text.relocate(line.getBoundsInParent().getMinX() + line.getBoundsInParent().getWidth()/2 - text.prefWidth(-1) / 2, line.getBoundsInParent().getMinY() - 30);
}
By the way, I suggest creating a dedicated marker class for that which holds the line and the text instead of using a "loose" map.
Related
I need to move the label (Text) of all tick marks in an Axis such that the text is right in the middle of its own tick mark and the next tick mark.
I am using Roland's GanttChart control (with some modifications) and Christian Schudt's DateAxis for my X-axis. My objective is to plot a gantt chart based on Date values (ignoring time; all time values are truncated).
My gantt chart has a "start date" and an "end date" for every single task (i.e. visually on the chart it is represented by a single bar).
Consider this:
I have a task starting on 1st Feb, and it ends on the same day (1st Feb). I have two ways to render this on the chart:
Render it starting from 1st Feb, and ends at 1st Feb. This bar is effectively hidden, because its width is 0.
Render it starting from 1st Feb, with the right-edge of the bar touching 2nd Feb. This can potentially confuse the users because it would look like it starts from 1st Feb and ends on 2nd Feb.
To solve the problem caused by method 2, I need to shift the text labels by half a tick mark width to the right. Doing this would make it very clear to the user.
I have tried doing this:
final DateAxis xAxis = (DateAxis) this.ganttchart.getXAxis();
xAxis.getChildrenUnmodifiable().addListener(new ListChangeListener<Node>()
{
#Override
public void onChanged(javafx.collections.ListChangeListener.Change<? extends Node> c)
{
final List<Node> labels = xAxis.getChildrenUnmodifiable().filtered(node -> node instanceof Text);
for (Node label : labels)
{
label.setTranslateX(xAxis.getWidth() / (labels.size() - 1) / 2);
}
}
});
However, doing so does not seem to shift the Text labels to the right at all.
The method I had tried actually works, but I was having problem with race condition. If I used Platform.runLater(), it would shift correctly. However, the position flickers whenever I resize the chart (it would jump between original position and shifted position).
This is the complete working version, which I need to override layoutChildren() in DateAxis.
#Override
protected void layoutChildren()
{
if (!isAutoRanging())
{
currentLowerBound.set(getLowerBound().getTime());
currentUpperBound.set(getUpperBound().getTime());
}
super.layoutChildren();
/*
* Newly added codes.
*/
final List<Node> labels = this.getChildrenUnmodifiable().filtered(node -> node instanceof Text);
for (Node label : labels)
{
if (this.getSide() == Side.LEFT || this.getSide() == Side.RIGHT)
label.setTranslateY(this.getHeight() / (labels.size() - 1) / 2);
else if (this.getSide() == Side.TOP || this.getSide() == Side.BOTTOM)
label.setTranslateX(this.getWidth() / (labels.size() - 1) / 2);
}
/*
* End of new codes.
*/
}
Update
There was still some layout bugs. It has to do with the fact that Axis.positionTextNode() positions the texts using getBoundsInParent(), which takes into consider the translation.
This is the new working version, hopefully someday it would be helpful to someone.
#Override
protected void layoutChildren()
{
if (!isAutoRanging())
{
currentLowerBound.set(getLowerBound().getTime());
currentUpperBound.set(getUpperBound().getTime());
}
super.layoutChildren();
/*
* Newly added codes.
*/
final List<Node> labels = this.getChildrenUnmodifiable().filtered(node -> node instanceof Text);
for (Node label : labels)
{
if (this.getSide().isHorizontal())
{
if (label.getTranslateX() == 0)
label.setTranslateX(this.getWidth() / (labels.size() - 1) / 2);
else
{
label.setLayoutX(label.getLayoutX() + label.getTranslateX());
}
}
else if (this.getSide().isVertical())
{
if (label.getTranslateY() == 0)
label.setTranslateY(this.getHeight() / (labels.size() - 1) / 2);
else
{
label.setLayoutY(label.getLayoutY() + label.getTranslateY());
}
}
}
/*
* End of new codes.
*/
}
I'm trying to make a simple 3-column TableView:
One icon column containing a fixed size icon. This column must not be resizable, and must have a fixed size.
One text column with a predefined prefered size, which can be resized if needed.
One last column taking all available space.
Unfortunatly, this simple use case seems to be really complicated with Java FX 8. I tried the following, which should work according to my understanding of the documentation:
TableColumn<DebuggerItem, ImageView> iconColumn = new TableColumn<>("ICON");
TableColumn<DebuggerItem, String> typeColumn = new TableColumn<>("TEXT");
TableColumn<DebuggerItem, String> textColumn = new TableColumn<>("DATA");
setColumnResizePolicy(CONSTRAINED_RESIZE_POLICY);
// Fixed size column
iconColumn.setPrefWidth(40);
iconColumn.setMinWidth(40);
iconColumn.setMaxWidth(40);
iconColumn.setResizable(false);
// Predefined preferred size of 100px
typeColumn.setPrefWidth(100);
getColumns().addAll(iconColumn, typeColumn, textColumn);
This results in the following TableView:
We can see that if the first column has a correct size, the second and the hird have the same size, which is not what I expected. The second column should be 100px wide, and the last one take the rest of the space.
What did I miss ?
According to #kleopatra link, the solution is to use properties to compute last column width, and NOT use CONSTRAINED_RESIZE_POLICY :
LastColumnWidth = TableViewWidth - SUM(Other Columns Widths)
Which, according to my example give the following Java code:
TableColumn<DebuggerItem, ImageView> iconColumn = new TableColumn<>("ICON");
TableColumn<DebuggerItem, String> typeColumn = new TableColumn<>("TEXT");
TableColumn<DebuggerItem, String> textColumn = new TableColumn<>("DATA");
// Fixed size column
iconColumn.setPrefWidth(40);
iconColumn.setMinWidth(40);
iconColumn.setMaxWidth(40);
iconColumn.setResizable(false);
// Predefined preferred size of 100px
typeColumn.setPrefWidth(100);
// Automatic width for last column
textColumn.prefWidthProperty().bind(
widthProperty().subtract(
iconColumn.widthProperty()).subtract(
typeColumn.widthProperty()).subtract(2)
);
getColumns().addAll(iconColumn, typeColumn, textColumn);
Please note that we need to substract 2 pixels to get the exact width, it's not clear why.
I created a more general solution, that pixel-perfectly calculates width of last column for TableViews that:
have any number of columns
possibly hide or show columns at runtime
possibly have custom insets
possibly have scrollbar, possibly showing and hiding at runtime.
Usage:
bindLastColumnWidth(tableView);
Source:
public static <T> void bindLastColumnWidth (TableView<T> tableView) {
List<TableColumn<T,?>> columns = tableView.getColumns();
List<TableColumn<T,?>> columnsWithoutLast = columns.subList(0, columns.size() - 1);
TableColumn lastColumn = columns.get(columns.size() - 1);
NumberExpression expression = tableView.widthProperty();
Insets insets = tableView.getInsets();
expression = expression.subtract(insets.getLeft() + insets.getRight());
for (TableColumn column : columnsWithoutLast) {
NumberExpression columnWidth = Bindings.when(column.visibleProperty())
.then(column.widthProperty())
.otherwise(0);
expression = expression.subtract(columnWidth);
}
ScrollBar verticalScrollBar = getScrollBar(tableView, Orientation.VERTICAL);
if (verticalScrollBar != null) {
NumberExpression scrollBarWidth = Bindings.when(verticalScrollBar.visibleProperty())
.then(verticalScrollBar.widthProperty())
.otherwise(0);
expression = expression.subtract(scrollBarWidth);
}
expression = Bindings.max(lastColumn.getPrefWidth(), expression);
lastColumn.prefWidthProperty().bind(expression);
}
private static ScrollBar getScrollBar (Node control, Orientation orientation) {
for (Node node : control.lookupAll(".scroll-bar")) {
if (node instanceof ScrollBar) {
ScrollBar scrollBar = (ScrollBar)node;
if (scrollBar.getOrientation().equals(orientation)) {
return scrollBar;
}
}
}
return null;
}
You can call this below code after you add all columns to table.
This is nice when the code that resizes does not have a explicit list of columns.
public static void makeLastColumnGrow(TableView<?> items) {
var last = items.getColumns().get(items.getColumns().size() - 1);
var aBinding = Bindings.createDoubleBinding(() ->{
double width = items.getWidth();
for (int i = 0; i < items.getColumns().size() - 1 ; i++) {
width = width - items.getColumns().get(i).getWidth();
}
return width - 2; // minus -2 to account for border i think
}, items.widthProperty());
last.prefWidthProperty().bind(aBinding);
}
Is there any way I can draw an arrow at the current position or add an arrow to a paragraph like a chunk to get something like:
This arrow points to the right -> thats it!
How can I draw "->" as arrow like in MS Word inline?
EDIT: if someone needs it:
public final Font SYMBOL_FONT = new Font(Font.getFamily(BaseFont.SYMBOL));
public Chunk getArrow(final boolean paramLeft) {
Chunk arrow;
if (paramLeft) {
arrow = new Chunk(String.valueOf((char)220), SYMBOL_FONT);
} else {
arrow = new Chunk(String.valueOf((char)222), SYMBOL_FONT);
}
return arrow;
}
I am looking for a way to display text progressively with libgdx, but I can't find a way to do it exactly the way I want. Here is what I did:
I have a text label that is being updated periodically to display a different text. The label is set to setWrap(true); and setAlignment(Align.center);
Every time I change the text of the label I use a custom Action which I built like this
public class DisplayTextAction extends TemporalAction{
private CharSequence completeText;
#Override
protected void update(float percent) {
((Label)actor).setText(
completeText.subSequence(
0,
(int)Math.round(completeText.length()*percent));
}
public void setText(String newText){
completeText = newText;
}
}
Every text update, I call the action from a pool, change the text and add the action to the label.
Here is my problem: This doesn't work the way I want with a centered and wrapped text.
This happens when text isn't centered (dots represent space):
|h........|
|hel......|
|hello....|
(Works as intended)
This is what happens when the text is centered:
|....h....|
|...hel...|
|..hello..|
And this is how I want it to behave:
|..h......|
|..hel....|
|..hello..|
My original idea to fix this was to use 2 sets of strings, one that is the visible text, and one invisible that acts as "padding". I came up with something like this:
CharSequence visibleText = completeText.subSequence(
0,
(int)Math.round(completeText.length()*percent));
CharSequence invisibleText = completeText.subSequence(
(int)Math.round(completeText.length()*percent),
completeText.length());
So I have my two sets of strings, but I can't find a way to display two different fonts (one visible, and another one which is the same but with an alpha of 0) or styles in the same label with Libgdx.
I'm stuck, I don't know if my approach is the right one or if I should look into something completely different, and if my approach is correct, I don't know how to follow it up using libgdx tools.
EDIT:
I followed Jyro117's instructions and I could make great progress, but I couldn't make it work with centred text on multiple lines.
imagine this text:
|all those lines are|
|..for a very long..|
|........text.......|
And it has to be displayed like this
|all th.............|
|...................|
|...................|
|all those line.....|
|...................|
|...................|
|all those lines are|
|..for a ve.........|
|...................|
|all those lines are|
|..for a very long..|
|........text.......|
Jyro117's solution give either
|all those lines are|
|for a very long....|
|text...............|
displayed correctly.
or
|...................|
|......all tho......|
|...................|
|...................|
|...all those lin...|
|...................|
|all those lines are|
|......for a v......|
|...................|
You are over-complicating the solution. All you really need is to determine the size of the label when all the text is added. Once you have determined that, lock the label size to those dimensions, put it inside of a table that expands to fill up the area around it, and then update your label with the action. (You can use a pool and such as needed, but for simplicity I left that out of the code below).
You will have to obviously adapt the code to yours, but this gives you a code reference to what I mean.
Here is a code snippet on one way to do it:
stage = new Stage(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false);
Gdx.input.setInputProcessor(stage);
uiSkin = new Skin(Gdx.files.internal("skin/uiskin.json"));
Table fullScreenTable = new Table();
fullScreenTable.setFillParent(true);
final String message = "hello";
final Label progressLabel = new Label(message, this.uiSkin);
final TextBounds bounds = progressLabel.getTextBounds(); // Get libgdx to calc the bounds
final float width = bounds.width;
final float height = bounds.height;
progressLabel.setText(""); // clear the text since we want to fill it later
progressLabel.setAlignment(Align.CENTER | Align.TOP); // Center the text
Table progressTable = new Table();
progressTable.add(progressLabel).expand().size(width, height).pad(10);
final float duration = 3.0f;
final TextButton button = new TextButton("Go!", this.uiSkin);
button.addListener(new ClickListener() {
#Override public void clicked(InputEvent event, float x, float y) {
progressLabel.addAction(new TemporalAction(duration){
LabelFormatter formatter = new LabelFormatter(message);
#Override protected void update(float percent) {
progressLabel.setText(formatter.getText(percent));
}
});
}
});
stage.addActor(button);
fullScreenTable.add(progressTable);
fullScreenTable.row();
fullScreenTable.add(button);
stage.addActor(fullScreenTable);
Edit:
Added code to center and top align text in label. Also added code to fill spaces on the end to allow for proper alignment. Note: Only useful for mono-spaced fonts.
class LabelFormatter {
private final int textLength;
private final String[] data;
private final StringBuilder textBuilder;
LabelFormatter(String text) {
this.textBuilder = new StringBuilder();
this.data = text.split("\n");
int temp = 0;
for (int i = 0 ; i < data.length; i++) {
temp += data[i].length();
}
textLength = temp;
}
String getText(float percent) {
textBuilder.delete(0, textBuilder.length());
int current = Math.round(percent * textLength);
for (final String row : data) {
current -= row.length();
if (current >= 0) {
textBuilder.append(row);
if (current != 0) {
textBuilder.append('\n');
}
} else {
textBuilder.append(row.substring(0, row.length() + current));
// Back fill spaces for partial line
for (int i = 0; i < -current; i++) {
textBuilder.append(' ');
}
}
if (current <= 0) {
break;
}
}
return textBuilder.toString();
}
}
I am busying writing a simple application using JavaFX2. The goal is just to plot 2 nodes (the nodes are movable by dragging them) and then have a function to draw lines between these nodes.
I finished the functions to add and move nodes (at the moment I am just using Ellipse shapes but I am going to replace it later with my own node class) but now I am struggling with the connecting lines. The actions to add a node or a line is from a dropdown menu and I have the following code on the line function:
private void drawLine(MenuItem line) {
final BooleanProperty lineActive = new SimpleBooleanProperty(false);
final BooleanProperty clickOne = new SimpleBooleanProperty(false);
final BooleanProperty clickTwo = new SimpleBooleanProperty(false);
line.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent t) {
lineActive.set(true);
}
});
nodeGroup.setOnMousePressed(new EventHandler<MouseEvent>() {
public void handle(final MouseEvent t1) {
clickOne.set(true);
if (lineActive.get()) {
if (clickOne.get()) {
//get x and y of first node
x1 = ((Ellipse) t1.getTarget()).getCenterX();
y1 = ((Ellipse) t1.getTarget()).getCenterY();
clickOne.set(false);
clickTwo.set(true);
}
if (clickTwo.get()) {
nodeGroup.setOnMouseClicked(new EventHandler<MouseEvent>() {
public void handle(MouseEvent t2) {
//get x and y of second node
x2 = ((Ellipse) t2.getTarget()).getCenterX();
y2 = ((Ellipse) t2.getTarget()).getCenterY();
//draw line between nodes
final Line line = new Line();
line.setStartX(x1);
line.setStartY(y1);
line.setEndX(x2);
line.setEndY(y2);
canvas.getChildren().add(line);
clickTwo.set(false);
lineActive.set(false);
}
});
}
}
}
});
}
I just have the booleans to check for the first and second click to get the center of each node.
My first question is when I click on the line function and add a line between 2 nodes, it doesn't seem to end the function, and any other nodes I click on gets a line to it. How can I prevent it from executing more than once.
And my second question is how can I "connect" the line to the nodes that if the node moves, the line stays in the center of the node?
Thanks.
I think there is a couple of things which could make this simpler...
Do not use booleans when you have more than two states (no click, click one, click two) use an enum instead. Then you only need one variable to look after.
Only ever set one mouse listener on the nodeGroup and check which state you're in and have the appropriate code there instead of separate mouse listeners.
I imagine that the program is setting the listener for the second click and not resetting it to the listener for the first click when it completes.
Since i am new to Stack Overflow , I tried something on your problem
First add line between two labels
final Line line = new Line();
#Override
public void initialize(URL arg0, ResourceBundle arg1)
{
// TODO Auto-generated method stub
line.setStartX(lblDragTest.getLayoutX());
line.setStartY(lblDragTest.getLayoutY());
line.setEndX(lblNew.getLayoutX());
line.setEndY(lblNew.getLayoutY());
rootAnchorPane.getChildren().add(line);
}
And then add this methods...
// This code handles label move
//set lblDragMousePressed method to Mouse Pressed event for lblDrag
#FXML
public void lblDragMousePressed(MouseEvent m)
{
System.out.println("Mouse is pressed");
prevLblCordX= (int) lblDragTest.getLayoutX();
prevLblCordY= (int) lblDragTest.getLayoutY();
prevMouseCordX= (int) m.getX();
prevMouseCordY= (int) m.getY();
}
//set this method on mouse released event for lblDrag
#FXML
public void lblDragMouseReleased(MouseEvent m)
{
System.out.println("Label Dragged");
}
// set this method on Mouse Drag event for lblDrag
#FXML
public void lblDragMouseDragged(MouseEvent m)
{
diffX= (int) (m.getX()- prevMouseCordX);
diffY= (int) (m.getY()-prevMouseCordY );
int x = (int) (diffX+lblDragTest.getLayoutX()-rootAnchorPane.getLayoutX());
int y = (int) (diffY+lblDragTest.getLayoutY()-rootAnchorPane.getLayoutY());
if (y > 0 && x > 0 && y < rootAnchorPane.getHeight() && x < rootAnchorPane.getWidth())
{
lblDragTest.setLayoutX(x);
lblDragTest.setLayoutY(y);
}
line.setStartX(lblDragTest.getLayoutX());
line.setStartY(lblDragTest.getLayoutY());
line.setEndX(lblNew.getLayoutX());
line.setEndY(lblNew.getLayoutY());
// rootAnchorPane.getChildren().add(line);
}