Make a JSVGCanvas inside JSVGScrollPane match the SVGDocument size - java

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;
}
// ...
}

Related

JTree nodes only show top half of panel

I have a JTabbedPane with several tabs. Inside each tab I have a JScrollpane with a jtree inside. Each jtree node is another panel, containing other components, returned by a function. This works fine.
The problem is this:
On first show of the trees (all of them) the root nodes do not show completely. Only the top half shows. when double-clicked they show correctly if they have child nodes, if they don't nothing happens.
I can't find a way to show the nodes correctly the first time. I tried invalidate, validate paint, repaint, etc with several combinations.
Nothing works.
Found it!
ScrollPaneContainigTheTabel.setColumnHeader(new JViewport()
{
/**
*
*/
private static final long serialVersionUID = 1L;
#Override public Dimension getPreferredSize()
{
Dimension d = super.getPreferredSize();
d.height = WHATEVER_HEIGHT_IS_NEEDED;
return d;
}
});
Forcing the Table Header height solved the problem...
Now it recalculates the sizes correctly...
WHATEVER_HEIGHT_IS_NEEDED can be an int or a calculated value if you use (as i do) html to set more than one line in the header...

java jpanel synchronization with repaint() or: Is a Listener addable to swing's repaint?

I'm writing a plugin for a miscropy program and have problems with the repaint() method.
short question:
Is there any way to get informed as soon as the repaint of a JPanel was done or synchronize the code with it?
detailed version:
My program can plot a set of data in a xy-chart to a JPanel and show it using jfree.chart; In another part of the programm I have many datasets (~100) that I want to plot and save as images. I've also found a solution, but I really don't like it. The Problem can be reduced to a notification about the paint status of a JPanel.
In the part that shall save all images I have this solution:
PlotSpectrum spectrumWindow = getTheWindow(); //pseudo code...
// some stuff
ti = storage.getImage(channel, slice, frame, position);
spectrumWindow.plotData(false, andor.captureSpectrum(ti.pix), wave,
centerWave, fineGrating, exposureTime,
slitWidth, substractBackground);
spectrumWindow.repaint(); // probably not necessary
sleep(100); // this annoys me...
spectrumWindow.savePlot(path, true, config, null);
spectrumWindow is a JPanel that is also displayed in another window and it all works fine.
BUT I really don't like that sleep(100) in there... without it I'm asking for a repaint but it isn't done till I try to save a "snapshot" of (thats what savePlot is doing...). I know, other Thread and these damn synchronization problems...
With the sleeping I'm just making it unnecessary slow and if I wait not long enough the images are not completly drawn (eg lower half missing)
Is there any way to get informed as soon as the repaint was done? I probably would be also fine with a Listener, better would be a solution with a monitor or sth comparable or a method that is repainting NOW (doesn't exists as far I know?)
The main GUI (include the JPanel spectrumWindow) and the earlier pasted code are running in different Threads.
The probably also important parts of my code are following here. Please excuse if some brackets aren't matching or some variables aren't declared, I removed very much code.
thanks
schetefan24
class PlotSpectrum extends ApplicationFrame // that extends JFrame
{
public void plotData(boolean backgroundGiven, int[] spect, double[] wave_,
double centerWave, boolean fineGrating_, double exposureTime,
double slitWidth, boolean substractBackground)
{
//process data and store internally
replot();
}
private void replot()
{
XYSeries series = new XYSeries("Spectrum");
//add data to series
XYSeriesCollection collection = new XYSeriesCollection(series);
//setting up some labels and such stuff...
JFreeChart chart = ChartFactory.createXYLineChart(
title,
"Wavelength [nm]",
yLabel,
collection,
PlotOrientation.VERTICAL,
false,
false,
false
);
dataPanel.add(new ChartPanel(chart)); // this is contained in a Frame
}
public void savePlot(String path, boolean overWriteAll, HashMap<String,String> config, int[][] addData)
{
File output = new File(path);
//some more stuff, ask overwrite etc
if(image)
{
BufferedImage im = createImage();
String extension = path.substring(path.lastIndexOf(".")+1, path.length());
ImageIO.write(im, extension, output);
} else {
//that is an textexport, works fine
}
}
public BufferedImage createImage()
{
JPanel panel = (JPanel) flipChart.getSelectedComponent();
int w = panel.getWidth();
int h = panel.getHeight();
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = bi.createGraphics();
panel.paint(g);
return bi;
}
}
that I want to plot and save as images.
add the data to a non visible panel.
create a BufferedImage of the panel
create an ImageIcon using the Image from above
update a JLabel (that has already been added to the frame) using the setIcon(...) method
the above step should generate a PropertyChange event when the Icon changes. You can use a ProperChangeListener to listen for this event. When you receive the event you can repeat steps 1 - 4.
Check out Screen Image. It will help you create an image of a non-visible component.
Note, you don't really need steps 4-5. I just added them so you have a visual of the plots as they are being processed. If you don't want the visual then maybe you just display text on a JLabel indicating which plot is currently being converted.

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

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 - 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().

Get the size of a Node in a given layout in javafx 2.0?

In the skin of a custom control I would like to draw a triangle the size of the control, and have the triangle grow as the frame is resized. I have the following code but the bounds only increase in size as I resize the frame. How to I get it to resize correctly?
private void update()
{
Bounds bounds = node.getBoundsInParent();
Path path = new Path();
path.getElements().add(
new MoveTo(
bounds.getWidth() / 2 + bounds.getMinX(),
bounds.getMinY()));
path.getElements().add(
new LineTo(bounds.getMaxX(), bounds.getMaxY()));
path.getElements().add(
new LineTo(bounds.getMinX(), bounds.getMaxY()));
path.setFill(Color.RED);
node.getChildren().setAll(path);
}
Edit: Using swing I would do the following. But I can't get it to work in JavaFX.
public class Arrow extends JPanel
{
#Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Dimension size = getSize();
Point top = new Point(size.width/2,0);
Point bottomRight = new Point(size.width, size.height);
Point bottomLeft = new Point(0, size.height);
GeneralPath path = new GeneralPath();
path.moveTo(top.x, top.y);
path.lineTo(bottomRight.x, bottomRight.y);
path.lineTo(bottomLeft.x, bottomLeft.y);
path.lineTo(top.x, top.y);
Graphics2D g2d = (Graphics2D)graphics.create();
g2d.setColor(Color.RED);
g2d.fill(path);
g2d.dispose();
}
}
In the skin of a custom control I would like to draw a triangle the size of the control, and have the triangle grow as the frame is resized.
The ScrollBar thumb implementation in the default Caspian style for JavaFX does exactly this. It does it via a -fx-shape css attribute:
.scroll-bar:vertical .increment-arrow {
-fx-background-color: -fx-mark-highlight-color, -fx-mark-color;
-fx-background-insets: 1 0 -1 0, 0;
-fx-padding: 0.333333em 0.5em 0.0em 0.0em; /* 4 6 0 0 */
-fx-shape: "M -3 0 L 0 4 L 3 0 z";
}
Documentation of -fx-shape is:
An SVG path string. By specifying a shape here the region takes on that shape instead of a rectangle or rounded rectangle. The syntax of this path string.
Now to your apparently unrelated question title:
Get the size of a Node in a given layout in javafx 2.0?
So what size do you really want?
The Node's visual bounds is it's bounds in parent.
The Node's layout bounds is:
The rectangular bounds that should be used for layout calculations for this node. layoutBounds may differ from the visual bounds of the node and is computed differently depending on the node type.
If you don't use the -fx-shape css stuff I mentioned earlier (for what you are doing), you probably want to use layout bounds as you will be laying out your Triangle within the parent Region of the Control and your Triangle would automatically inherit any transforms applied to the parent Region.
Layout for a Node is often not completely calculated until it has been added to an active scene and a css pass been executed on the Node. So you can add a listener to the appropriate property (e.g. boundsInLocal) and update your triangle rendering whenever the listener gets fired. This can be done without creating a custom control and skin.
If you do go the custom control and skin route, then you can override your control's layoutChildren method and do your layout work in there - but this is a fairly sophisticated, advanced use case, that is often unnecessary unless you are trying to create performance critical, reusable controls.

Categories