How to make ticker(animated text) in javafx [duplicate] - java

I am trying to achieve effect similar to marquee - line of long (in my case) text which is moved in horizontal axis. I managed to get it work, but I can't call it satisfactory.
My Controller class looks as below:
#FXML
private Text newsFeedText;
(...)
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
TranslateTransition transition = TranslateTransitionBuilder.create()
.duration(new Duration(7500))
.node(newsFeedText)
.interpolator(Interpolator.LINEAR)
.cycleCount(Timeline.INDEFINITE)
.build();
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
int width = gd.getDisplayMode().getWidth();
transition.setFromX(width);
transition.setToX(-width);
transition.play();
}
newsFeedText is binded to some text source which is dynamically updated, so it contains various amount of text.
My code has at least two drawbacks:
Transition goes from -width to +width; width is monitor's resolution width
There will be moments when text will not be visible at all if window is not full-screened.
If text will be longer and newsFeedText width will be greater than monitor's resolution width then transition will disappear "in half" (still being on a screen).
Currently Duration is not dependent on a width of newsFeedText.
Now, it's nothing worng, but if transition's fromX and toX were be dynamically calculated then it will result in various speeds of marquee.
How to get rid of these drawbacks?

I have managed it to work, any recalculations can happen only after transition is stopped so we cannot set its cycleCount to Timeline.INDEFINITE. My requirement was that I could change text inside component so there are fxml wirings:
#FXML
private Text node; // text to marquee
#FXML
private Pane parentPane; // pane on which text is placed
The code which works is:
transition = TranslateTransitionBuilder.create()
.duration(new Duration(10))
.node(node)
.interpolator(Interpolator.LINEAR)
.cycleCount(1)
.build();
transition.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
rerunAnimation();
}
});
rerunAnimation();
where rerunAnimation() is:
private void rerunAnimation() {
transition.stop();
// if needed set different text on "node"
recalculateTransition();
transition.playFromStart();
}
and recalculateTransition() is:
private void recalculateTransition() {
transition.setToX(node.getBoundsInLocal().getMaxX() * -1 - 100);
transition.setFromX(parentPane.widthProperty().get() + 100);
double distance = parentPane.widthProperty().get() + 2 * node.getBoundsInLocal().getMaxX();
transition.setDuration(new Duration(distance / SPEED_FACTOR));
}

You should be able to do this by listening to your scene's widthProperty. You can either access this via newsFeedText.getScene().widthProperty() or get a reference from your main class and expose it from there or pass it to a method or constructor to access within your class that declares newsFeedText.
The benefit of this approach is that now your logic is dependent upon the width of your scene (a dynamic dependency) rather than the width of your monitor (a static dependency). Note that I have not tested this approach but at the moment see no reason (perhaps naively) it shouldn't work.
As for your duration dependency, you can solve that by performing some sort of calculation based on the length of the text in newsFeedText. Something like Duration.seconds(newsFeedText.get Text().length()/denominator) where denominator is some value you specify (such as 7500, as in your code). This will make your duration dynamically computed based on the length of your text.
If you want to operate with the width of newsFeedText itself, rather than the length of its text, then simply replace newsFeedText.getText().length() with newsFeedText.getWidth(). Ensure you perform this computation after newsFeedText has been laid out so a call to get its width returns the actual width. You can also replace the call with any of getPrefWidth(), getMinWidth(), or getMaxWidth().

Related

Pre-set position of spawned node

I'm trying to make app that spawns new draggable nodes on pretty big pane(which is child of scrollpane), but this node should be spawned in the center of the screen.
Q is: Are there any methods to pre-set X,Y coordinates of these new imageviews?
For example:
button.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent e) {
Bounds bounds = scrollPane.getBoundsInLocal();
Bounds screenBounds = scrollPane.localToScreen(bounds);
int mX = (int) screenBounds.getMinX();
int mY = (int) screenBounds.getMinY();
Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
int x = (int) ((primScreenBounds.getWidth() - mX) /4);
int y = (int) ((primScreenBounds.getHeight() - mY) /4);
/*
System.out.println("X coords:" +x);
System.out.println("Y coords:" +y);
*/
pane.getChildren().addAll(new ImageView(imgvw.getImage()));
//somehow set coordinates of new ImageView
}
});
The way you'd set the position deĀ“pends on the layout you use. However assuming you use Pane, you could use
layoutX and layoutY or
translateX and translateY
ImageView iv = new ImageView(imgvw.getImage());
iv.setLayoutX(x);
iv.setLayoutY(y);
pane.getChildren().add(iv);
Other layouts, e.g. StackPane automatically set layoutX and layoutY. In this case you could set managed to false
iv.setManaged(false);
or use the appropriate parameters for the layout type.
This might depend on the parent the ImageViews are placed in, though in general you should be able to position Nodes with Region#positionInArea() (Region is superclass of Parent). Note that this is a protected method, meaning you might want to create your own parent (e.g. by extending StackPane for example).
That being said, there are plenty of Parent Nodes, each providing their own unique behavior. Try to make sure that the desired behavior cannot be achieved using any of those before creating your own implementation. (And since you dont specify any positioning behavior, its hard to make recommendations)

Setting event listener for two JTextArea's

I am developing an image processing software (just for fun) and one of the features it has is image resizing option. Basically window pops up, with two JTextArea components to get desired image width and height for resizing. There is also JCheckBox for keeping Aspect ratio if user desires it. The problem is. When check box is selected and user supposedly inputs either width or height first. I want the other text area to update itself accordingly every time a change is made so it would keep AR. I have developed some code that deals with this, but it does not provide what I really want due to lack of understanding what listener to what component should I really assign.
Code:
String height, width;
if (checkBoxImage.isSelected()){
// aspect ratio = width / height
width = widthArea.getText();
height = heightArea.getText();
double aspectRatio = (double) images.get(tabbedPane.getSelectedIndex()).getWidth() / images.get(tabbedPane.getSelectedIndex()).getHeight();
/**
* to do, update width, height area
* to the closest user input
*/
if(heightArea.getText().length() != 0 && heightArea.getText().length() <= 5
&& heightArea.getText().charAt(0) != '0'){
//parsing string to integer
try{
int heightNum = Integer.parseInt(height);
int widthNum = (int) Math.round(aspectRatio * heightNum);
widthArea.setText(String.valueOf(widthNum) );
widthArea.updateUI();
frameimgSize.repaint();
}
catch(NumberFormatException e1){JOptionPane.showMessageDialog(error,e1.getMessage(),"Error", JOptionPane.ERROR_MESSAGE);}
}
//width has been entered first
else if(widthArea.getText().length() != 0 && widthArea.getText().length() <= 5 &&
widthArea.getText().charAt(0) != '0'){
try{
int widthNum = Integer.parseInt(width);
int heightNum = (int) Math.round(aspectRatio * widthNum);
heightArea.setText(String.valueOf(heightNum) );
heightArea.updateUI();
frameimgSize.repaint();
}
catch(NumberFormatException e1){JOptionPane.showMessageDialog(error,e1.getMessage(),"Error", JOptionPane.ERROR_MESSAGE);}
}
}
Is it ever valid to have non-numeric values in your width and height fields?
If not, then use JSpinners or JFormattedTextFields instead of JTextFields. If so, (say for example you allow a "units" to be entered as well as width and height) you should attach a DocumentListener to your JTextFields to monitor changes to the content of the underlying text documents. Here's an example:
widthField.getDocument().addDocumentListener(new DocumentListener() {
public void changedUpdate(DocumentEvent e) {
update();
}
public void removeUpdate(DocumentEvent e) {
update();
}
public void insertUpdate(DocumentEvent e) {
update();
}
// your method that handles any Document change event
public void update() {
if( aspectCheckBox1.isSelected() ) {
// parse the width and height,
// constrain the height to the aspect ratio and update it here
}
}
});
You'd then add a similar DocumentListener to your heightTextField.
Note that if you use JTextFields you need to parse their contents, read the units (where applicable) and handle NumberFormatExceptions in the case where the user enters invalid numeric values.
To answer your question about where to add your handlers...
The update of the Width should happen when there is a Document change to the Height GUI element. Similarly the update of the Height should happen when there is a document change to the Width GUI element.
You'll need to gracefully handle divide by zero errors (or restrict input to always be greater than 0), perform your calculations using doubles and preferably use Math.round() to get the best integer values for preserving aspect.
ie:
int calculateHeight(int width, double aspect) {
if( aspect <= 0.0 ) {
// handle this error condition
}
return (int)Math.round(width / aspect);
}
For actually tracking the aspect ratio, I would store it in a member variable and add an ActionListener to the JCheckBox... because updating the target aspect ratio on every value change of the width and height fields could result in aspect-ratio "creeping" due to integer round-off.
Here's an example on tracking your aspect every time the aspect ratio check state changes:
private double aspect = 1.0;
aspectCheckBox.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
preserveAspectActionPerformed(evt);
}
});
private void preserveAspectActionPerformed(java.awt.event.ActionEvent evt) {
try {
double w = Double.parseDouble(widthField.getText());
double h = Double.parseDouble(heightField.getText());
aspect = w / h;
}
catch(NumberFormatException ex) {
// ... error occurred due to non-numeric input
// (use a JSpinner or JFormattedTextField to avoid this)
}
}
The most important thing is to avoid using the wrong input type for the job:
JTextAreas are good for multiline text
JTextFields are good for single line text
JFormattedTextFields are good for text constrained to specific format
JSpinners are good for numeric entry.
Hope that helps you.
First, I wouldn't use JTextArea, it's meant for free form text editing (think NotePad). Instead you should, at the very least, use a JTextField but a JSpinner might actually even be better.
Take a look at How to Use Text Fields for more details.
Essentially, for JTextField, you could use a ActionListener and/or a FocusListener to monitor for changes to the field.
This listeners will tend to be notified after the fact, that is, only once the user has finished editing fields. If you want real time feed back, you could use a DocumentListener which will notifiy each time the underlying Document of the field is modified, in real time.
A JSpinner is a little more complicated as it's a component that contains an editor and controls. You can use a ChangeListener, which will notifiy when a change to the fields model is commited. This occurs in place of the ActionListener and FocusListener mentioned previously, so you should only require a single listener, but won't provide real time feedback (at least, not without a lot more work)

JavaFX - horizontal marquee text

I am trying to achieve effect similar to marquee - line of long (in my case) text which is moved in horizontal axis. I managed to get it work, but I can't call it satisfactory.
My Controller class looks as below:
#FXML
private Text newsFeedText;
(...)
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
TranslateTransition transition = TranslateTransitionBuilder.create()
.duration(new Duration(7500))
.node(newsFeedText)
.interpolator(Interpolator.LINEAR)
.cycleCount(Timeline.INDEFINITE)
.build();
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
int width = gd.getDisplayMode().getWidth();
transition.setFromX(width);
transition.setToX(-width);
transition.play();
}
newsFeedText is binded to some text source which is dynamically updated, so it contains various amount of text.
My code has at least two drawbacks:
Transition goes from -width to +width; width is monitor's resolution width
There will be moments when text will not be visible at all if window is not full-screened.
If text will be longer and newsFeedText width will be greater than monitor's resolution width then transition will disappear "in half" (still being on a screen).
Currently Duration is not dependent on a width of newsFeedText.
Now, it's nothing worng, but if transition's fromX and toX were be dynamically calculated then it will result in various speeds of marquee.
How to get rid of these drawbacks?
I have managed it to work, any recalculations can happen only after transition is stopped so we cannot set its cycleCount to Timeline.INDEFINITE. My requirement was that I could change text inside component so there are fxml wirings:
#FXML
private Text node; // text to marquee
#FXML
private Pane parentPane; // pane on which text is placed
The code which works is:
transition = TranslateTransitionBuilder.create()
.duration(new Duration(10))
.node(node)
.interpolator(Interpolator.LINEAR)
.cycleCount(1)
.build();
transition.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
rerunAnimation();
}
});
rerunAnimation();
where rerunAnimation() is:
private void rerunAnimation() {
transition.stop();
// if needed set different text on "node"
recalculateTransition();
transition.playFromStart();
}
and recalculateTransition() is:
private void recalculateTransition() {
transition.setToX(node.getBoundsInLocal().getMaxX() * -1 - 100);
transition.setFromX(parentPane.widthProperty().get() + 100);
double distance = parentPane.widthProperty().get() + 2 * node.getBoundsInLocal().getMaxX();
transition.setDuration(new Duration(distance / SPEED_FACTOR));
}
You should be able to do this by listening to your scene's widthProperty. You can either access this via newsFeedText.getScene().widthProperty() or get a reference from your main class and expose it from there or pass it to a method or constructor to access within your class that declares newsFeedText.
The benefit of this approach is that now your logic is dependent upon the width of your scene (a dynamic dependency) rather than the width of your monitor (a static dependency). Note that I have not tested this approach but at the moment see no reason (perhaps naively) it shouldn't work.
As for your duration dependency, you can solve that by performing some sort of calculation based on the length of the text in newsFeedText. Something like Duration.seconds(newsFeedText.get Text().length()/denominator) where denominator is some value you specify (such as 7500, as in your code). This will make your duration dynamically computed based on the length of your text.
If you want to operate with the width of newsFeedText itself, rather than the length of its text, then simply replace newsFeedText.getText().length() with newsFeedText.getWidth(). Ensure you perform this computation after newsFeedText has been laid out so a call to get its width returns the actual width. You can also replace the call with any of getPrefWidth(), getMinWidth(), or getMaxWidth().

JavaFX Popup under Node

Resources and examples for this Popup widget are vague.
Suppose I have a random Node somewhere on the stage. How do I open a Popup exactly under it (e.g. like a dropdown menu, but with other nodes inside it).
I'm trying to avoid boilerplate code (i.e. fine-tuning the position myself).
Update 1:
Either Point2D point = node.localToScene(0.0, 0.0); does not work as I imagine it should, or I'm using it wrong.
Update 2:
See here a simple example, but lacking the functionality I'm needing
Let's say you have the node node
you can get its position by
Point2D point = node.localToScene(0.0, 0.0);
// now get point.getX() and point.getY() here
Considering the example that you have given (in Update 2):
I removed this bit:
popup.setX(300);
popup.setY(200);
and modified this code:
show.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
popup.show(primaryStage);
Point2D point = show.localToScene(0.0, 0.0);
popup.setX(primaryStage.getX() + point.getX());
popup.setY(primaryStage.getY() + point.getY() + 40);
// this 40 could be show.getPrefHeight() if height of button is set
}
});
Since Popup is a separate window, you need to set its position by adding the offset of the Stage.

Make a JSVGCanvas inside JSVGScrollPane match the SVGDocument size

I am having a problem with displaying my SVG drawing in an diagramming application. The canvas takes the size of its parent component, instead of the document it contains. Documents that are bigger than this size are not completely rendered.
Case
I have a drawing that is, for example, 621x621 pixels.
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" contentScriptType="text/ecmascript" zoomAndPan="magnify" contentStyleType="text/css" preserveAspectRatio="xMaxYMax slice" version="1.0">
<rect x="50" y="50" fill="white" width="20" height="20" stroke="black" stroke-width="1"/>
<rect x="600" y="600" fill="blue" width="20" height="20" stroke="black" stroke-width="1"/>
</svg>
It has no viewBox because it is created dynamically. When I'm creating the document I don't have the size.
I have a JSVGCanvas inside a JSVGScrollPane, the parent of the scrollpane is smaller then the document, lets say its 500x500 pixels.
Expected behaviour
When you launch the application the part of the drawing that fits in the 500x500 is visible. The underlying document is bigger so there are scrollbars that allow me to scroll 121px to the left and to below.
When you increase the size of the frame, more and more of the drawing becomes visible. When you increase it to be greater than 620x620px the scrollbars can disappear completely.
If you make the panel smaller then the document again, the scrollbars reappear.
Encountered behaviour
When you launch the application the part of the drawing that fits in the 500x500 is visible. No scrollbars are visible.
When you make the panel bigger, the canvas is increased (its background color) but the drawing is not extended to the part that's outside the initial 500x500px.
When you make the panel smaller, lets say to 400x400, scrollbars do appear, but only allow me to scroll 100px. So the canvas still is the initial 500x500 instead of the desired 621x621.
Example
The encountered behaviour can be reproduced with the ScrollExample on the Batik repository and the SVG-file i've added above. The example uses a fixed size of 500x500px.
If you'd add background colors to the canvas and scrollpane, the entire frame is blue. So while the canvas does increase in size, it does not draw anything beyond its initial size.
canvas.setBackground(Color.blue);
scroller.setBackground(Color.red);
I could reset the document when I've re-sized the panel. This makes it redraw the visible part of the drawing, but the scrolling is still not working as expected, since the canvas is still just the size of the visible portion instead of the size of the document. You can test this by adding this listener to the frame (or any other panel I suppose).
class ResizeListener implements ComponentListener {
JSVGCanvas canvas;
URI url;
public ResizeListener(JSVGCanvas c, URI u) {
canvas = c;
url = u;
}
#Override
public void componentResized(ComponentEvent arg0) {
canvas.setURI(url.toString());
// Calling invalidate on canvas or scroller does not work
}
// ...
};
I have read that I should have a viewBox, but my document is recreated often (based on a modelchange-event) and when I'm creating the document, i don't know how to get the viewBox. SVGDocument.getRootElement().getBBox(); usually gives me null. There are supposed to be fallbacks in the Batik code when no viewBox is given, so I hope this is optional. Also when I'm panned somewhere, it should keep this panning if I change the document.
I hope i'm making my problem clear. Am I expecting too much of the JSVG-classes when I expect them to provide my desired behaviour out of the box or am I missing something here? Could someone please guide me towards a solution?
I think I might have found a solution. Instead of messing with viewBox I need to set the height and width of the document. With the size set, no viewBox is necessary and the scrollbars work as expected. If I do set a viewbox, the canvas keeps scaling the image to make it fit.
Now, for updating the size I use this whenever I change the dom that could have influence on the size.
static BridgeContext ctx = new BridgeContext(new UserAgentAdapter());
static GVTBuilder builder = new GVTBuilder();
private static void calculateSize(SVGDocument doc) {
GraphicsNode gvtRoot = builder.build(ctx, doc);
Rectangle2D rect = gvtRoot.getSensitiveBounds();
doc.getRootElement().setAttributeNS(null,
SVGConstants.SVG_WIDTH_ATTRIBUTE, rect.getMaxX() + "");
doc.getRootElement().setAttributeNS(null,
SVGConstants.SVG_HEIGHT_ATTRIBUTE, rect.getMaxY() + "");
}
I do this outside of the UpdateManager thread and after the dom modification I use JSVGCanvas.setSVGDocument(). When I tried to do this via the UpdateManager it only updated the first change. There is probably a fix for this with the right initialization of the updatequeue, but since I change a lot of the document, I could just as well start fresh every time.
I am left with one minor issue. Whenever i set a new document, the scroll position is reset. I tried getting the transformation before the manipulation and reapply it afterwards, but this doesn't seem to work.
Edit+:
I managed to fix this minor issue by replacing the root node of the document rather than replacing the entire document. The canvas is set with the documentstate ALWAYS_DYNAMIC. The changes in the document are applied immediately, including the new size of the root node (set as described above). But because the document is not entirely replaced the scroll state is kept.
The canvas is only ready for modification of the dom after it has finished rendering a document. I used the SVGLoad-listener to detect this.
class Canvas extends JSVGCanvas implements SVGLoadEventDispatcherListener {
public Canvas() {
super();
setDocumentState(ALWAYS_DYNAMIC);
addSVGLoadEventDispatcherListener(this);
}
/**
* Indicatates whether the canvas has finished its first render, the
* canvas is now ready for modification of the dom
*/
private boolean isReadyForModification = false;
/**
* Renew the document by replacing the root node with the one of the new
* document
*
* #param doc The new document
*/
public void renewDocument(final SVGDocument doc) {
if (isReadyForModification) {
getUpdateManager().getUpdateRunnableQueue().invokeLater(
new Runnable() {
#Override
public void run() {
// Get the root tags of the documents
Node oldRoot = getSVGDocument().getFirstChild();
Node newRoot = doc.getFirstChild();
// Make the new node suitable for the old
// document
newRoot = getSVGDocument().importNode(newRoot,
true);
// Replace the nodes
getSVGDocument().replaceChild(newRoot, oldRoot);
}
});
} else {
setSVGDocument(doc);
}
}
#Override
public void svgLoadEventDispatchCompleted(SVGLoadEventDispatcherEvent e) {
isReadyForModification = true;
}
// ...
}

Categories