If you are not redirected to JXLayer 4.0 demonstration by Piet Blok please press the link.

JXLayer and a generic ZoomUI

This blog is out dated. Please go to JXLayer demonstration by Piet Blok for a new version with completely restructured code.

December 6, 2008.

Updated: December 18 2008. Finally solved the problem where neighbors were destroyed by the painting of the ZoomPort. And also solved the MouseEvent re dispatch glitch.

Updated: December 19 2008. Finally solved the autonomous repainting by descendant components. A custom RepaintManager ensures that for all those descendants, the ZoomUI is set dirty when they themselves are marked as (partly) dirty. The "Auto repaint" option has been removed. Also solved an issue with locations near the border of the ZoomPort when the scale is less than zero. Since the components located at these locations fall officially outside the bounds of the ZoomPort, SwingUtilities.getDeepestComponentAt(...) could not locate them. I included a slightly customized version of this method in the ZoomUI that overcomes this difficulty

Updated: December 21 2008. Boosted performance by using the clip area size for the intermediate image size.

The ZoomUI doesn't inherit from GeneralLayerUI anymore, but directly from AbstractLayerUI (I saw no advantages and it complicated a couple of things). In a later stage I will decide where to store the transformation parameters (currently in the ZoomUI). Possibly a better place will be the ZoomPort, making the ZoomUI more transparent and in no need at all to inherit from GeneralLayerUI.

Updated: December 23 2008. Completely reorganized the code. The functionality is now organized as follows:

Updated: December 24 2008. A small performance increase and moved the SetRenderingHints method to ZoomPort.

Updated: January 2 2009. Solved an issue with CardLayout. The private implementation of getDeepestComponentAT in ZoomUI didn't test for isVisible().

Updated: January 11 2009. Solved the caret position issue. Abraham, thank you so much for the pointer! For all components extending JTextComponent, the Document property "i18n" is set to Boolean.TRUE (done in ZoomPortLayout in the layoutContainer() method). This causes a different GlyphPainter to be used that does calculations in floating point precision. I detected two unwanted effects:

  1. The text is displayed slightly lower, causing the lower pixels of the letter "g" to become invisible.
  2. A strange "bumping effect" effect in the html page in the right JTextPane: try to enter somewhere, for example between "aaa" and "bbb" some letters "i", and you will see what I mean.
To have a check with the reference gui, I also applied the "i18n" property to those documents. You will see that the same anomalies happen there, so I'm pretty sure it is not my wrong doing, but some bug in the GlyphPainter (or related stuff).


A previous blog on JXLayer by me can be found here: JXLayer, a first impression

Some time ago there were some threads on the java dot net forums that discussed the possibility to create some sort of generic zoom panel: a panel that could show any component hierarchy larger or smaller. Generic ZoomPanel? and JXLayer for a ZoomPanel: odd painting behavior...

I find questions like these very interesting, so I thought about it for a while. Yes, visual zooming in and out is not that difficult, but what if the targeted component hierarchy has some life of its own and depend on all kinds of events that trigger actions that the generic, encapsulating, zoom panel is not aware of at all? And what with mouse events that are dispatched to components according to their physical location and not to their visual location?

Initially I thought that capturing mouse events and redispatching them to the intended components would be the most difficult part. Yes, I found it quite difficult to accomplish just that, but there were more problems to overcome.

A general outline

First, let me give some general outline how I solved the generic zoom panel. I created three classes that all play their role:

  1. A ZoomPort class, more or less analogous to JViewPort. Like JViewPort it can have only one child component, the view . A view can be any component hierarchy. Because the wrapping JXLayer can be the view port view of a JScrollPane, ZoomPort implements the Scrollable interface.
  2. A ZoomPortLayout , a LayoutManager for the ZoomPort. Its task is to layout the view component centered in the ZoomPort, and set the view size such that it can be zoomed in and out.
  3. A ZoomUI extending (indirectly, because I use a slightly changed GeneralLayerUI) from AbstractLayerUI. The ZoomUI is responsible for all zooming operations, including re dispatching of mouse events and setting the cursor on the glass pane, according to the intended component. The ZoomUI maintains a reference to itself in the ZoomPort, so that, when needed, the ZoomPort can delegate some tasks to the ZoomUI.

Since everything that is related to zooming boils down to transformations, I decided to also implement rotations and shearing. Only to discover later what nightmares that would trigger.

The problems I met and how I tried to solve them

When working on the ZoomUI, I met a variety of problems that I tried to solve. During these efforts I constantly moved functionality from one part of the system to other parts of the system. Sometimes a problem seemed solved, but invariably a new problem popped up, or a problem that was previously solved regressed again. The methodology used can better be described as "trial and error" than as "scientific".

Scaling to a smaller than original size

Initially, I implemented the paintLayer(...) method in ZoomUI, where I transformed the Graphics object according to the required zoom level. This worked fine for scale of 1.0 and greater, but failed miserably for a scale less than 1.0. What happened? The scaling worked indeed, but also the clip area for the painting of the ZoomPort view was scaled , resulting in only the center of the view visible.

I finally solved this issue by overriding the paintChildren(..) method of ZoomPort, and applying the transform in that method. Now I could safely call the super implementation.

The paintLayer method of AbstractLayerUI is not overridden. Note however that all transformational calculations are still in the ZoomUI class: when the ZoomPort needs a transformation, it is resolved by a method call to ZoomUI. There are two types of transformation needed:

  1. Transformation of a Graphics object when painting
  2. Transformation of a Dimension when calculating the preferred size

Mouse event re dispatching

Obvious, mouse events are generated from the actual position of the mouse on the screen and the assumed location of components by the JVM which is quite different from the location as the user sees it after some graphical transformation. So, mouse events must be examined to construct new mouse events with different coordinates and different target components.

This is done as follows:

  1. The eventDispatched method in ZoomUI is overridden to process all reported events. After processing of any event, setDirty(true) is invoked.
  2. A state is being maintained to distinguish between the incoming of original mouse events (as triggered by the JVM) and mouse events that are created by ZoomUI.
  3. All original MouseEvents are indiscriminately consumed so that they can do no more harm.
  4. Then, new MouseEvents are constructed with the correct target and location. Those new events are dispatched to the intended target.
  5. Those new events are also captured by the eventDispatched method. Of course they are not consumed. Instead, they are used to copy the Cursor from their target to the glass pane.

MouseEntered and MouseExited events are consumed but for the rest ignored, because they have no valid meaning. New MouseEntered and MouseExited events are constructed based on MouseMotion events. Also, MousePressed, MouseReleased, and MouseDragged events require some special handling. A previous glitch seems to be solved.

The MouseWheelEvent

The new GeneralLayerUI implements a more sophisticated way to re dispatch MouseWheelEvents. In the new version it searches up in the hierarchy to find the first component that has at least one MouseWheelListener registered. This enables to have JScrollPanes in the view component hierarchy and also have a JScrollPane as the parent of the JXLayer. Since no processMouseXXXX event is implemented in ZoomUI, some special code is implemented in ZoomUI to call the super.processMouseWheelEvent.

Clipping

When rotation and shearing is enabled, the ZoomUI is plagued by a clipping problem. You can see this when using the JXLayer in a JScrollPane. The scroll bars, and sometimes even the menu bar, will be over painted.

I solved it in the ZoomPort as follows:.

  1. Override the paint(Graphics g) method.
  2. Create a compatible BufferedImage with the size of the ZoomPort.
  3. Fill the background (if the ZoomPort is opaque) and print the transformed view on the image.
  4. Draw the image on the Graphics object.

Autonomous component painting

Some components behave quite autonomously. They don't wait for a parent that paints them, but decide for themselves when they want painting. As an example, think of the blinking caret in a JTextComponent.

As a result, the painting shows in the wrong place in the UI (not the scaled position) which causes nasty artifacts. Well, not always, but sometimes. I failed to figure out under exactly what circumstances this occurs.

I found no other way to reduce these artifacts than to introduce a Swing Timer that with some interval sets the ZoomUI to dirty.

In the demo you can enable/disable this function with the button Auto repaint . It starts a timer with a delay of 300 ms.

The problem is solved by a custom RepaintManager. The Swing Timer for repaints has been removed from the code.

The caret position

I noticed that the calculation of the caret position in JTextFields may be confused by a magnification factor. Attempts to resolve this by setting RenderingHints made it even worse. You will observe this when entering text into the upper or lower JTextField.

Strangely enough, the right JTextPane seems not to suffer from this phenomenon. In an earlier version, when using a JEditorPane, it had the same problem.

SOLVED!


The demo

The demo will show three frames:

  1. In the center of the screen a test frame that shows the demo contents via JXLayer and the ZoomUI
  2. In the upper left corner of the screen a control frame that allows the user to change some settings of the ZoomUI
  3. Adjacent to the control frame a reference frame that shows the demo content in a normal way. It is intended to verify that behavior on the test frame is consistent with behavior in normal mode.

Web Start Wrapped JXLayer

Here are the sources: PBZoom_src.zip


JXLayer is a tool developed by Alexander Potochkin , also known as alexp or alexfromsun .

This is the JXLayer project .

This is the Forum: JXLayer .


Since this is not a professional web site, just a bunch of hacked html pages on my personal home machine, there is no possibility to leave messages here. As an alternative, you may wish to leave a message in this thread on the JXLayer forum where I announced the experiment.

Author: Piet Blok. If you wish to contact me, please send email to email