Home > database >  JLabels are not drawing correctly in complicated drawing process
JLabels are not drawing correctly in complicated drawing process

Time:12-25

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:

Render result

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:

  1. 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.
  2. If I move the basicTest() call to main() method, nothing is changed.
  3. If I clear the panel contents and the document's logical model after basicTest() call and then call it again, and leave the code in componentResized() 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.
  4. 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.

  • Related