Search code examples
javaswingpaintcomponentjtabbedpane

How can I fix this code bug?


I am trying to make an image editing application in Java using the MVC design pattern. So, event handling is in a controller, state and operations relevant to the state are stored in models, and everything the user sees is stored in views.

When I open an image, there is a zoom box that displays the zoom level of the image being displayed. The zoom is automatically calculated when it is first rendered in paintComponent() (see step #3). When I open the image, I want the zoom level to be set to what it was calculated to be. The problem is that the zoom level shows 0, and I know why. Let me explain:

1. The ActionListener for the open menu item is fired

in JPSController:

class MenuBarFileOpenListener implements ActionListener {
    public void actionPerformed(ActionEvent event) {
        File fileChooserReturnValue = view.showAndGetValueOfFileChooser();

        if (fileChooserReturnValue != null) {
            try {
                // irrelevant code omitted
                view.addDocument(newDocument);
            } catch(IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

2. view.addDocument() is called

Note: this is the root of the problem

in JPSView:

public void addDocument(DocumentModel document) {
    // irrelevant code omitted

    // CanvasPanelView extends JPanel
    documentsTabbedPane.add(newDocumentView.getCanvasPanelView());

    // THIS IS THE ROOT OF THE PROBLEM
    double currentZoomFactor = getCurrentCanvasPanelView().getZoomFactor();

    // formatting the text
    String zoomLevelText = statusBar_zoomLevelTextField_formatter.format(currentZoomFactor);
    // setting the text of the text field
    statusBar_zoomLevelTextField.setText(zoomLevelText);
}

3. Some time later, paintComponent() is run

in CanvasPanelView extends JPanel:

public void paintComponent(Graphics g) {
    super.paintComponent(g);

    if (initialRender) {
        initialRender = false;

        // calculates a good zoom level
        setZoomFit();
    }

    // irrelevant code omitted

    g.drawImage(image, destinationX1, destinationY1, destinationX2,
                destinationY2, sourceX1, sourceY1, sourceX2, sourceY2, null);
    }

In part 2, we have this line of code:

double currentZoomFactor = getCurrentCanvasPanelView().getZoomFactor();

When getZoomFactor() is called, the current CanvasPanelView must not have a size, because it returns 0. I had this problem before, and my solution was with these lines of code in #3:

if (initialRender) {
    initialRender = false;
    setZoomFit();
}

When paintComponent() is called, the CanvasPanelView must have been given a size by then, but not when getZoomFactor() was called. paintComponent(), and therefore also setZoomFit(), obviously come after getZoomFactor().

How can I correctly show the zoom level of the image when it is opened?


Solution

  • Basically, you have a kind of race condition (as much as you can in single thread).

    What's happening is you are adding the new view, expecting it to be laid out immediately. This is not how this works in Swing. When you add a new component to a container, a request is made to update the layouts of all the components in the hierarchy, and layout all those containers that have been marked as invalid. This won't happen until the current call cycle has completed and the Event Dispatching Thread has had time to process all the requests.

    Instead, you can schedule a look up at a later time by adding a request to the end of the Event Dispatching Thread. This ensures that your request will be executed after all the current tasks on the EDT have been executed before it (there may be others to follow, but you've squeezed in).

    The following example demonstrates the point.

    It adds a new panel to the JTabbedPane, dumps it's current size, use SwingUtilities.invokeLater to request a callback at a time in the future and dumps the panels size again. You should find that the first request is 0x0 and the second will be a valid value (depending on the size of the frame and the look and feel)

    import java.awt.BorderLayout;
    import java.awt.EventQueue;
    import java.awt.GridBagLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JTabbedPane;
    import javax.swing.SwingUtilities;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class TestTabbedPane01 {
    
        public static void main(String[] args) {
            new TestTabbedPane01();
        }
    
        private JTabbedPane tabbedPane;
    
        public TestTabbedPane01() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException ex) {
                    } catch (InstantiationException ex) {
                    } catch (IllegalAccessException ex) {
                    } catch (UnsupportedLookAndFeelException ex) {
                    }
    
                    tabbedPane = new JTabbedPane();
                    JButton btnAdd = new JButton("Add");
                    btnAdd.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            final JPanel panel = new JPanel(new GridBagLayout());
                            panel.add(new JLabel(String.valueOf(tabbedPane.getComponentCount())));
                            tabbedPane.add(String.valueOf(tabbedPane.getComponentCount()), panel);
                            System.out.println("New Panel Size = " + panel.getSize());
                            SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                    System.out.println("New Panel Size (later) = " + panel.getSize());
                                }
                            });
                        }
                    });
    
                    JFrame frame = new JFrame("Test");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.setLayout(new BorderLayout());
                    frame.add(tabbedPane);
                    frame.add(btnAdd, BorderLayout.SOUTH);
                    frame.setSize(200, 200);
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
    
    }