Zooming in JavaFx: ScrollEvent is consumed when content size exceeds ScrollPane viewport - zooming

I have an application that requires zoom inside a ScrollPane, but with my current approach I'm still facing 2 challenges. In order to replicate the problem, I have written a small application ZoomApp that you will find the code for underneath. Its' limited functionality allows zooming in and out (using Ctrl + Mouse Wheel) on some arbitrary shapes. When the zoomed content grows outside the bounds of the window, scroll bars should apperar.
Challenge 1.
When the scroll bars appears as a consequence of innerGroup scales up in size, the ScrollEvent no longer reach my ZoomHandler. Instead, we start scrolling down the window, until it reaches the bottom, when zooming again works as expect. I thought maybe
scrollPane.setPannable(false);
would make a difference, but no. How should this unwanted behaviour be avoided?
Challenge 2.
How would I go about to center innerGroup inside the scrollPane, without resorting to paint a pixel in the upper left corner of innerGroup with the desired delta to the squares?
As a sidenote, according to the JavaDoc for ScrollPane: "If an application wants the scrolling to be based on the visual bounds of the node (for scaled content etc.), they need to wrap the scroll node in a Group". This is the reason why I have an innerGroup and an outerGroup inside ScrollPane.
Any suggestions that guides me to the solution is much appreciated by this JavaFX novice.
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
/**
* Demo of a challenge I have with zooming inside a {#code ScrollPane}.
* <br>
* I am running JavaFx 2.2 on a Mac. {#code java -version} yields:
* <pre>
* java version "1.7.0_09"
* Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
* Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode)
* </pre>
* 6 rectangles are drawn, and can be zoomed in and out using either
* <pre>
* Ctrl + Mouse Wheel
* or Ctrl + 2 fingers on the pad.
* </pre>
* It reproduces a problem I experience inside an application I am writing.
* If you magnify to {#link #MAX_SCALE}, an interesting problem occurs when you try to zoom back to {#link #MIN_SCALE}. In the beginning
* you will see that the {#code scrollPane} scrolls and consumes the {#code ScrollEvent} until we have scrolled to the bottom of the window.
* Once the bottom of the window is reached, it behaves as expected (or at least as I was expecting).
*
* #author Skjalg Bjørndal
* #since 2012.11.05
*/
public class ZoomApp extends Application {
private static final int WINDOW_WIDTH = 800;
private static final int WINDOW_HEIGHT = 600;
private static final double MAX_SCALE = 2.5d;
private static final double MIN_SCALE = .5d;
private class ZoomHandler implements EventHandler<ScrollEvent> {
private Node nodeToZoom;
private ZoomHandler(Node nodeToZoom) {
this.nodeToZoom = nodeToZoom;
}
#Override
public void handle(ScrollEvent scrollEvent) {
if (scrollEvent.isControlDown()) {
final double scale = calculateScale(scrollEvent);
nodeToZoom.setScaleX(scale);
nodeToZoom.setScaleY(scale);
scrollEvent.consume();
}
}
private double calculateScale(ScrollEvent scrollEvent) {
double scale = nodeToZoom.getScaleX() + scrollEvent.getDeltaY() / 100;
if (scale <= MIN_SCALE) {
scale = MIN_SCALE;
} else if (scale >= MAX_SCALE) {
scale = MAX_SCALE;
}
return scale;
}
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
final Group innerGroup = createSixRectangles();
final Group outerGroup = new Group(innerGroup);
final ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(outerGroup);
scrollPane.setOnScroll(new ZoomHandler(innerGroup));
StackPane stackPane = new StackPane();
stackPane.getChildren().add(scrollPane);
Scene scene = SceneBuilder.create()
.width(WINDOW_WIDTH)
.height(WINDOW_HEIGHT)
.root(stackPane)
.build();
stage.setScene(scene);
stage.show();
}
private Group createSixRectangles() {
return new Group(
createRectangle(0, 0), createRectangle(110, 0), createRectangle(220, 0),
createRectangle(0, 110), createRectangle(110, 110), createRectangle(220, 110),
createRectangle(0, 220), createRectangle(110, 220), createRectangle(220, 220)
);
}
private Rectangle createRectangle(int x, int y) {
Rectangle rectangle = new Rectangle(x, y, 100, 100);
rectangle.setStroke(Color.ORANGERED);
rectangle.setFill(Color.ORANGE);
rectangle.setStrokeWidth(3d);
return rectangle;
}
}

OK, so I finally found a solution to my problem.
By merely substituting the line
scrollPane.setOnScroll(new ZoomHandler(innerGroup));
with
scrollPane.addEventFilter(ScrollEvent.ANY, new ZoomHandler(innerGroup));
it now works as expected. No mystic Rectangle or other hacks are needed.
So the next question is why? According to this excellent article on Processing Events,
An event filter is executed during the event capturing phase.
while
An event handler is executed during the event bubbling phase.
I assume this is what makes the difference.

The following workaround seems to be giving better results:
Set onscroll event on the outer group to steal the event from the scroll pane.
Add a opaque rectangle which covers the whole screen, so that you don't miss the scroll event. Apparently you can miss scroll event if you don't hit a shape.
Rectangle opaque = new Rectangle(0,0,WINDOW_WIDTH,WINDOW_HEIGHT);
opaque.setOpacity( 0 );
outerGroup.getChildren().add( opaque );
outerGroup.setOnScroll(new ZoomHandler(innerGroup));

In my case I have updated the following line
if (scrollEvent.isControlDown()) {
with if (!scrollEvent.isConsumed()) {.... along with the change you have posted.... :)

Related

Messy diagram derives when it comes to printing it on pdf by using MS Windows 10 service

I've been writing Java SE 8 desktop application. I use Eclipse IDE, Oracle's JDK, and run it on MS Windows 10 OS.
My app draws diagrams, in short. I draw a diagram on JPanel which becomes part of JTabbedPane. It displays it well on GUI, and it is very responsive. The problem shows up when I pass the diagram on printing service. But instead of printing it on printer, I choose, "Microsoft print to PDF" service. What happens next, is that if the diagram is large, when you scroll it down, you will observe that its quality drops down. That is, grids start disappearing, new lines appear, etc.
The pic out here.
As you can see, eventually vertical grids vanish, diagonal line creeps in, and later gets even worse. And that is unwelcome.
Relevant code in here:
public final class GanttChartDiagram extends TopDiagram{
#Override
public void paintComponent(Graphics graph){
super.paintComponent(graph);
Graphics2D g2D = (Graphics2D)graph;
g2D.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
...........
#Override
public Dimension getPreferredSize(){
return new Dimension(this.diagramWidth + 20, this.diagramHeight + 20);
}
}
}
The getPreferredSize() method identifies the size of diagram, so that the app knows how to adjust the scroll-bars accordingly to fit the diagram in. Otherwise by default it return 0, if not overridden.
That is the class where I draw the diagram.
The super-class out here:
public abstract class TopDiagram extends JPanel implements Printable {
private static final long serialVersionUID = 1469816888488484L;
#Override
public void paintComponent(Graphics graph){
super.paintComponent(graph);
};
/**
* Prints selected diagram
*/
#Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
Graphics2D dimension = (Graphics2D)graphics;
dimension.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
if(pageIndex < PrintingImageBuffer.getImageBuffer().size()){
dimension.drawImage(PrintingImageBuffer.getImageBuffer().get(pageIndex), null, 0, 0);
return PAGE_EXISTS;
}
else{
return NO_SUCH_PAGE;
}
}
}
Now that is where I print the diagram:
public static void printDiagram(){
new Thread(new Runnable(){
public void run(){
TopDiagram diagram = null;
if(id.equalsIgnoreCase("GanttChart")){
diagram = ganttChartDiagram;
}
final PrinterJob printer = PrinterJob.getPrinte
rJob();
printer.setJobName("Printing the "+id+" Diagram");
PageFormat format = printer.pageDialog(page);
int nowWidth = (int)diagram.getPreferredSize().getWidth();
int nowHeight = (int)diagram.getPreferredSize().getHeight();
BufferedImage buffImg = new BufferedImage(nowWidth, nowHeight, BufferedImage.TYPE_INT_ARGB);//default type
Graphics2D d = (Graphics2D)buffImg.getGraphics();
....etc..................
}
}).start();
}
Now the interesting part is this:
int nowWidth = (int)diagram.getPreferredSize().getWidth();
int nowHeight = (int)diagram.getPreferredSize().getHeight();
On the same instance of diagram, on multiple invocations of print (or within the method), it may or may not return different values. So that it was causing me some sort of Raster exception. But I managed to get rid off of that exception by invoking size method only once and reusing that size value throughout the method. So that, the size value stays the same, cause it is read only once.
Bad solution, but it works.
I would like to solve this issue too. Firstly, how come that this invocation diagram.getPreferredSize().getWidth() on the same instance of diagram obj. returns different size value? One more thing, is that I overrode this method as has been presented above. And the diagram object is created only once, no recalculations.
This is where I create the diagram obj. on Swing Worker only once per application's life-cycle.
GanttChartSwingWorker ganttSwingWorker = new GanttChartSwingWorker(GanttChartDiagram::new, tabbedPane, showPerformanceDiagramTab, ganttChartDiagramTabReady);
ganttSwingWorker.execute();
new Thread(() -> {
try{
ganttChartDiagramTabReady.await();
ganttChartDiagram = ganttSwingWorker.getGanttChartDiagram();
}catch(InterruptedException ie){ie.printStackTrace();}
}
).start();
Swing Worker part:
diagram = this.get();
JScrollPane scrollPaneChart = addScrollPane(diagram);
tabbedPane.addTab("Gantt Chart", null, scrollPaneChart, "Displays Gantt Chart Diagram");
Some diagram objects can be time consuming to create, imposes delay, so I use Swing Worker to do that.
So, in summary:
How to make the diagram to appear clean when I print/save it on pdf file in the way that I explained?
How to make the diagram size to calculate consistently as per multiple invocations? What leads to different diagram size values retrieving it from the same diagram object instance on multiple calls?
I just figured out what the problem is.
I observed that when it comes to drawing the diagram, the
paintComponent(Graphics g) method has been invoked repeatedly. It keeps redrawing the diagram over and over again. It is invoked by the system implicitly, yet my implementation had been triggering it.
And that trigger comes in the form of this.setSize(width, height) method on derived JPanel object. So that each time the paintComponent(Graphics g) re-executes, it sets the size on JPanel which yet triggers additional execution of the painComponent method. In the end, it is an infinite loop.
And that infinite execution was the cause of the problem; it was producing distorted diagram image on pdf file.
Solution: execute the setSize method only when it is needed; on initial panel set-up, or resize.

Double Buffering a JFrame

[Warning, I am a beginner, I have little knowledge about JFrame and I am just getting started, but your help would be greatly appreciated!]
So, here is my Problem: I am currently working on something extremely simple, just a red rectangle moving across the screen and I know how beginner-like that must sound.
My current Code:
package movement;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;
public class Movement extends JFrame {
public static void main(String[] args) {
Movement m = new Movement();
m.setSize(1000, 1000);
m.setTitle("Movement");
m.setBackground(Color.WHITE);
m.setVisible(true);
m.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void paint (Graphics g){
int width = 0;
int height = 0;
int x = 100;
int y = 900;
while(x < 1000 && y > 0){
//System.out.println("Success");
g.setColor(Color.RED);
g.fillRect(x+width, y-height, 200, 200);
//g.fillRect(x+width, y-height, 200, 200);
try{
Thread.sleep(10);
} catch(InterruptedException e){
}
g.setColor(Color.WHITE);
g.fillRect(x+width, y-height, 200, 200);
//g.fillRect(x+width, y-height, 200, 200);
width=width+1;
height=height+1;
}
}
}
So as you can see it works, but the image stutters and flicks a bit, since there is only a single Buffer. I heard that adding a JPanel would allow me to double buffer and have a way smoother experience, but since I am a real beginner I wouldn't know how to implement it here. I am not sure how a JPanel would help here and in which way to use it.
Swing is double buffered by default. You are overriding the paint() method of the frame and have not invoked super.paint(...) so you are losing the default double buffering.
I heard that adding a JPanel would allow me to double buffer and have a way smoother experience
Yes, this is the proper way to do custom painting. It is not difficult all you do is override the paintComponent() method of the JPanel and add the panel to the frame. Read the section from the Swing tutorial on Custom Painting for more information and examples.
Also, you should NEVER have a while loop with a Thread.sleep() in a painting method. To do animation you should use a Swing Timer. The tutorial link I provided above also has a section on Timers or you can search the forum/web for examples.

How to loop a tween/autoscroll TextField across screen

I'm working in Flash AS3, AIR 3.2 for iOS SDK. I'm trying to make a TextField appear and tween from the right side of the screen automatically to the left side, disappear, then reappear from the right again in a loop.
Trying to work out how I would even start to code this and the logic behind it. At the moment, I have a class called MarqueeTextField which is doing the scrolling. But it only scrolls/marquees letter character to character, and not from point to point on-screen. It also "stutters" when scrolling at slow speeds and not smooth.
public class MarqueeTextField extends TextField
{
/** Timer that ticks every time the marquee should be updated */
private var __marqueeTimer:Timer;
/**
* Make the text field
*/
public function MarqueeTextField()
{
__marqueeTimer = new Timer(0);
__marqueeTimer.addEventListener(TimerEvent.TIMER, onMarqueeTick);
}
/**
* Callback for when the marquee timer has ticked
* #param ev TIMER event
*/
private function onMarqueeTick(ev:TimerEvent): void
{
this.text = this.text.substr(1) + this.text.charAt(0);
}
/**
* Start marqueeing
* #param delay Number of milliseconds between wrapping the first
* character to the end or negative to stop marqueeing
*/
public function marquee(delay:int): void
{
__marqueeTimer.stop();
if (delay >= 0)
{
__marqueeTimer.delay = delay;
__marqueeTimer.start();
}
}
}
}
I've searched and there doesn't seem to be any that uses ONLY AS3 (they use the Flash GUI). Anyone able to explain to me how I'd begin to code this and make it smooth even at slow speeds?
If you really must, you can always use GreenSock's Tweening engine. Just import the package and simply use TweenLite.to(). Also, it looks like you're just inserting text instead of actually changing the textbox's position.

How to create customize title bar with close button on jFrame?

I want to create a customised title bar for my JFrame. I can remove the default title bar with
JFrame.setUndecorated(true)
Now i need to create a customised title bar for my JFrame with a close button?
Without having done that ever, I think I would go this way:
Indeed set the JFrame to undecorated
Extend JRootPane to add an additional field titleBar
Create a TitleBar component holding the title, the close button, etc...
Set a new LayoutManager on that JRootPane (have a look at JRootPane.RootLayout) and layout the components in the appropriate order (first the title bar, then below the menubar, then below the content pane)
Set an instance of that extends RootPane on your JFrame
There are maybe better ways.
I'm not quite sure of how you want to customize the close button, but maybe this can point you in the right direction: How can I customize the title bar on JFrame?
EDIT: Here's an updated working link to a forum about customizing his GUI and one user posted code on his creation of a simple GUI: Here
It looks like you can just modify his removeComponents method and create an addComponents method to fit your needs.
The Code According to the Above Link :
(Edited for Java 8)
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.logging.Level;
import java.util.logging.Logger;
class Testing {
public void buildGUI() throws UnsupportedLookAndFeelException {
JFrame.setDefaultLookAndFeelDecorated(true);
JFrame f = new JFrame();
f.setResizable(false);
removeMinMaxClose(f);
JPanel p = new JPanel(new GridBagLayout());
JButton btn = new JButton("Exit");
p.add(btn, new GridBagConstraints());
f.getContentPane().add(p);
f.setSize(400, 300);
f.setLocationRelativeTo(null);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
btn.addActionListener((ActionEvent ae) -> {
System.exit(0);
});
}
public void removeMinMaxClose(Component comp) {
if (comp instanceof AbstractButton) {
comp.getParent().remove(comp);
}
if (comp instanceof Container) {
Component[] comps = ((Container) comp).getComponents();
for (int x = 0, y = comps.length; x < y; x++) {
removeMinMaxClose(comps[x]);
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
new Testing().buildGUI();
} catch (UnsupportedLookAndFeelException ex) {
Logger.getLogger(Testing.class.getName()).log(Level.SEVERE, null, ex);
}
});
}
}
may Work Fine but what if user also Want to set a L&F
such as nimbus
There are really three ways to approach this:
Set the frame to undecorated and implement everything, which includes control buttons, snapping, resizing and moving.
Get the root pane of the JFrame and directly edit that pane. You will need to add the control buttons and the snapping behaviour.
Use JNI to get the window's handle at the creation of a JFrame to get the control of it's attributes. This is better explained in this post. I have also built a little project which is basically an extension of the JFrame class that handles everything that needs to be dealt with... This last approach does not break native functions like snapping and resizing. But you do need to create the control buttons again since you have a new title bar if you want to build it from scratch.

Prevent JPanel from cutting off subcomponents

I am trying to prevent a JPanle to get so small that it cuts off it subcomponents, is there anyway to enforce this? After reading this I tried (in jython):
frame= JFrame('example', size=(200,200))
pan = JPanel()
pan.add(JLabel('beer'))
pan.add(JButton('get one', actionPerformed=printer))
pan.setMinimumSize(panel.getPreferredSize())
frame.add(pan)
frame.visible = True
however the panel can still be shrunk so far that it cuts off its components (in a JFrame or here), even if I apply the minimum size to the frame. How does one prevent this? ( I assume the preferred size I am getting si not what I want since with the default layout manager it seems to be the size of the largest subcomponent, but if I change it to e.g. GridBagLayout it seems to get smaller 9apply minsize to frame and try)
You can use this on your JFrame (sorry it's Java, I am not familiar with Jython):
frame.setMinimumSize(frame.getPreferredSize());
Your code could work if the LayoutManager of the content pane of the JFrame enforces minimum sizes. However, by default you get a BorderLayout which does not. If you used frame.setMinimumSize(pan.getPreferredSize());, it will not work because the size of the frame includes its insets (ie, the size of the border of the frame) so the minimum size you are setting is actually too small.
Here is a small demo showing how you can make minimum size work on JFrame:
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class TestMinimumSizeOnFrame {
protected void initUI() {
JFrame frame = new JFrame("example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(200, 200);
JPanel pan = new JPanel();
pan.add(new JLabel("Some nice beer"));
pan.add(new JButton("get one"));
frame.add(pan);
frame.setVisible(true);
frame.setMinimumSize(frame.getPreferredSize());
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TestMinimumSizeOnFrame().initUI();
}
});
}
}