I'm writing a custom HTML renderer from scratch. It renders block, inline-block, inline elements, floats etc.
I made a Drawable
interface that extends JPanel and overrides paintComponent()
method. It has two subclasses: Block and Character.
My code is working this way: I have a root JPanel
called panel
, which is contained inside the WebDocument
Swing element, which extends JPanel
as well. All my blocks, including root HTML element, are added onto that panel and have coordinates (0,0) and span the full panel
's area. This way I can render overflowing content correctly.
Then, each element has a JPanel
called text_layer
inside it, that also has coordinates (0,0) and spans the full area. I don't remember why I did it this way, maybe because it was easier to implement text highlight on selection, or maybe I had some problems with ClearType antialiasing.
Also I have 2 kinds of Drawables: Blocks and Characters, both of them extend JPanel
. Every Block
has a type
field, that has only 2 values - for HTML elements and for text nodes. During layout process the Layouter
entity creates lines for each block, and every block has its own array of Line
entities. Line can contain any Drawable
in it in any quantity - a Block or a Character, but generally they can't be mixed, because only leaf nodes, that have a type
value of 'text', consist of Characters
, and they cannot contain Blocks
inside them.
Finally, each Character
element extends JPanel
class, and positions itself in some arbitrary spot, and not (0,0), unlike the Blocks
. It also has a very constrained size.
It has a glyph
JLabel field inside it, which represents a single letter, and the glyph
is added into the Character
(that is a JPanel
) and is positioned at (0,0), so it is aligned with it perfectly.
It works just well in many cases (for example, I can have a Block
with type 'element', and 3 Blocks
with type 'text' inside it, they contain Characters
, which in turn contain glyph
JLabels, and everything works fine, including my custom text selection).
But it stops working when I have an 'element' Block
with another 'element' Block
inside it - no matter, 'block' or 'inline' it is in terms of HTML layout.
Sometimes the text in innermost blocks just won't show up, and sometimes the text does render, but the selection does not work at all (the selection is performed by changing background of glyph
JLabels). I mean, I see debug messages, that correct letters are being selected, but nothing does visually change.
And sometimes the text becomes either semi-transparent or very, very black after repaint (the repaint is triggered by mouse clicks and drags, for example). The second situation with very black and bold text (like the old glyph
JLabels are not removed, even though I explicitly remove them from text_layers
) can be seen if I add every text_layer
JPanel directly to the root JPanel in WebDocument
. If I add text_layers
to their 'owner' Blocks
, the first situation with semi-transparent text in several first Blocks
can be seen.
Also I have to note that the paintComponent()
method, that in turn calls my custom draw()
method, is overridden only for Blocks
, and not for Characters
. So Characters
do not draw themselves, they just act as the containers for JLabels
.
I don't know how to debug it and how to make a minimal working example in this situation. Can you please help or give an advice where to look at?
UPDATE: I'm very close to the solution, but still need help with the fix.
The problem with text semi-transparency of JLabels (letters) and with custom text selection is completely gone when I remove layout and repaint in my own method. You see, I have a resize handler on my WebDocument
, which is triggered when the component is resized. It is absolutely needed to adjust the content to the window resizing by user.
But when the test program is launched, I have two "layouts" and "repaints" going on at the same time. That's why I see such things:
I have already tried using volatile flag isPainting
and making my own paint/repaint methods synchronized. But for some reason it did not help.
So, when I leave the layout and repaint calls only in componentResized()
component listener, it works fine. But this is not the proper solution, because my test methods are self-contained. I mean, I need to be able to clear the canvas and call another method at any time, so the layout and repaint calls should stay there, too.
The layout methods are already synchronized. So, I can't really understand what is going on and why it is overlapping.
Here are critical parts of the code:
public LayoutTests() {
super("Simple component test");
document = new WebDocument();
JPanel cp = new JPanel();
cp.setLayout(new BoxLayout(cp, BoxLayout.PAGE_AXIS));
bp = new JPanel();
bp.setLayout(new BoxLayout(bp, BoxLayout.LINE_AXIS));
document.setBorder(BorderFactory.createLineBorder(Color.black, 1));
document.panel.setBackground(Color.WHITE);
setContentPane(cp);
cp.add(document);
cp.add(bp);
document.width = 478;
document.height = 300;
//document.panel.setBounds(0, 0, cp.width, cp.height);
//document.root.setBounds(1, 1, cp.width-2, cp.height-2);
document.root.addMouseListeners();
//bp.setBounds(9, 283, 474, 86);
Block root = document.root;
root.setBounds(1, 1, document.width, document.height);
root.setWidth(-1);
root.height = document.height-2;
root.viewport_height = root.height;
root.orig_height = root.height;
root.max_height = root.height;
//root.setBounds(root.getX(), root.getY(), root.width, root.height);
document.debug = true;
// Here the web document is getting laid out and painted
basicTest();
document.getParent().invalidate();
document.repaint();
btn = new JButton("Close");
btn.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
//btn.setBounds((bp.getWidth() - btn.getPreferredSize().width) / 2, (bp.getHeight() - btn.getPreferredSize().height)-10, btn.getPreferredSize().width, btn.getPreferredSize().height);
bp.add(Box.createHorizontalGlue());
bp.add(btn);
bp.add(Box.createHorizontalGlue());
cp.setBorder(BorderFactory.createEmptyBorder(9, 10, 9, 10));
cp.setPreferredSize(new Dimension(494, 370));
pack();
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
addComponentListener(new java.awt.event.ComponentAdapter() {
@Override
public void componentMoved(java.awt.event.ComponentEvent evt) {}
@Override
public void componentResized(java.awt.event.ComponentEvent evt) {
int w = getWidth();
int h = getHeight();
Insets insets = getInsets();
if (insets != null) {
w -= insets.left insets.right;
h -= insets.top insets.bottom;
}
document.setBounds(pad[0], pad[1], w - pad[0] * 2, h - 93);
bp.setBounds(pad[0], h - 38, w - pad[0] * 2, 30);
document.resized();
}
});
That's my constructor.
public void resized() {
//panel.repaint();
width = getWidth();
height = getHeight();
panel.setBounds(borderSize, borderSize, getWidth() - borderSize * 2, getHeight() - borderSize * 2);
if (getWidth() != last_width || getHeight() != last_height) {
root.setBounds(borderSize, borderSize, width - borderSize * 2 - root.getScrollbarYSize(), height - borderSize * 2 - root.getScrollbarXSize());
root.document.ready = false;
root.width = width - borderSize * 2 - 2;
root.height = height - borderSize * 2 - 2;
root.viewport_width = !root.hasVerticalScrollbar() ? root.width : root.width - root.getScrollbarYSize();
root.viewport_height = !root.hasHorizontalScrollbar() ? root.height : root.height - root.getScrollbarXSize();
root.orig_height = (int) (root.height / root.ratio);
root.max_height = root.height;
root.document.ready = true;
//System.err.println("Root width: " root.width);
//System.err.println("Viewport width: " root.viewport_width);
final Block instance = root;
if (root.document.ready && !root.document.isPainting && !resizing) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
// Delay here is just for testing purposes
Thread.sleep(1500);
resizing = true;
root.performLayout();
root.forceRepaintAll();
resizing = false;
JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(instance);
frame.getContentPane().repaint();
} catch (InterruptedException ex) {
Logger.getLogger(WebDocument.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
}
}
last_width = getWidth();
last_height = getHeight();
}
And that's the callback for the resize component event. Note that if I remove the invokeLater()
wrap, the bug is still present, but until the root panel is empty until the timeout is reached. With invokeLater()
I can see the initial document version instantly, so I guess it is better to keep it.
Here is the layout code in general ()note that the method calls itself recursively on different Block nodes and even on the same one sometimes to update its scrollbar presence/absence state):
public synchronized void performLayout() {
if (document.inLayout && this == document.root) {
try {
// Another layout is in progress, try to wait a little bit
Thread.sleep(50);
System.out.println("Retrying layout...");
} catch (InterruptedException ex) {
Logger.getLogger(Block.class.getName()).log(Level.SEVERE, null, ex);
}
performLayout();
return;
}
document.inLayout = true;
// The actual element layout is performed in here
performLayout(false, false);
document.inLayout = false;
}
And finally the custom drawing/painting code:
public synchronized void forceRepaintAll() {
if (document.isPainting) {
try {
// Another repaint is in progress, try to wait a little bit
Thread.sleep(50);
System.out.println("Retrying repaint...");
} catch (InterruptedException ex) {
Logger.getLogger(Block.class.getName()).log(Level.SEVERE, null, ex);
}
forceRepaintAll();
return;
}
document.isPainting = true;
if (document.debug) {
System.out.println("Complete repaint started");
}
// Searching for root; we could just use document.root field,
// but this way we can process detached elemkent subtrees, so why not
Block b = this;
while (b.parent != null) {
b = b.parent;
}
// Flush temporary buffers
b.flushBuffersRecursively();
// Actual painting goes here
b.draw();
if (document.debug) {
System.out.println("Complete repaint finished");
}
document.isPainting = false;
}
I tried experimenting a bit more, and here is what I found:
- If I completely comment out the code in
componentResized()
method, nothing is printed at all - I guess that's because I don't have the right dimensions set for my Swing containers hardcoded, so one of the dimensions can be just set to zero in my code. - If I move the
basicTest()
call tomain()
method, nothing is changed. - If I clear the panel contents and the
document
's logical model afterbasicTest()
call and then call it again, and leave the code incomponentResized()
in place, I have NOT working text selection, but the semi-transparency of the first text element is gone. Also in this case the text in the second purple outlined block seems bolder than it should be. - If I leave the "re-init" code from (3), but comment out the
componentResized()
again, nothing is painted.
As I can see now, I have no output at all if I remove my componentResized()
method or the resize event listener assignment.
That's strange, because it was painting the content just fine 20 minutes ago even without the resize handler - maybe because I was using invokeLater()
calls after sleep
in the beginning of layout/paint methods, I don't know.
As a result:
I see that the text selection and the text transparency (it should NOT be transparent) are broken when I have two layout/repaint call "pairs" on different threads: in constructor (Main) thread or anonymous thread from invokeLater()
from LayoutTest
class, and from the AWT-EventQueue Thread / anonymous thread from invokeLater()
in the document.resize()
method.
These two threads are different anyway, so I should get rid from one of them. But if I get rid from the second one, nothing is displayed, even though all the dimensions of the viewport are hardcoded. So I have to keep the second call, and re-implement the logic in my class.
Maybe keep a Map
somewhere, which will tell, which test case we are wanting to display. And then call the single test()
method which will render the current test, and not call test()
in constructor, leaving it to be called by onComponentResized()
code. And keep the resize handler in place.
CodePudding user response:
I found a bug in my code, and it was not Swing related. The problem was with the parts
vector where I stored fragments of inline blocks (to split single text spans on line boundaries). Because I forgot to clear the vector and to remove the fragments (Block/JPanel instances) from my Swing hierarchy, after the second layout call everything got broken.
Thanks for trying to help, guys.