0

I have a JMenuBar for my Swing app which helps the user carry out some common tasks. One of the JMenus in the menu bar is a "Font" menu, which loads all of the system's available fonts and lets the user change the font on a JTextArea. For each font, a separate JMenuItem is added to the JMenu whose font is respective to its name. I load the font in a SwingWorker's doInBackground method and then publish each item.

The class might help you understand: Edit: here's the minimum reproducible example:

public class MRE extends JFrame {

    public MRE() {
        setLayout(new BorderLayout());
        setSize(500, 500);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JMenuBar menu = new JMenuBar();
        JMenu view = new JMenu("View");
        JMenu font = new JMenu("Font");
        JTextArea area = new JTextArea();
        final String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();

        new SwingWorker<Void, JMenuItem>() {
            @Override
            protected Void doInBackground() {
                JMenuItem item;
                for (String font : fonts) {
                    item = new JMenuItem(font);
                    Font f = new Font(font, Font.PLAIN, 14);
                    item.setFont(f);
                    item.addActionListener(new Listener(f, area));
                    publish(item);
                }
                return null;
            }

            @Override
            protected void process(List<JMenuItem> chunks) {
                for (JMenuItem item : chunks) {
                    font.add(item);
                }
            }
        }.execute();

        view.add(font);
        menu.add(view);
        add(menu, BorderLayout.NORTH);
        add(area, BorderLayout.CENTER);
    }

    private record Listener(Font f, JTextArea area) implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            area.setFont(f);
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new MRE().setVisible(true));
    }
}

The problem is that the UI blocks for a second or two when the user hovers over the menu whilst it's loading the font.

Edit: here's what it looks like on my Windows laptop. It is slow to load the first time, but all times after that it loads almost instantly: JMenu lagging when loading items

I suspect the problem is due to this reason: "multiple invocations to the publish method might occur before the process method is executed. For performance purposes all these invocations are coalesced into one invocation with concatenated arguments." -- this is part of the Javadoc for the publish() method inside the SwingWorker class; it states that, because there are many publish() calls in a short amount of time, they are chained together, thus the UI blocks because it's loading everything at once.

I've even tried loading each JMenuItem with its respective font into a List inside a static block, and then inside the constructor, I loop over the list and add the item, but this also blocks.

For both versions, removing the item.setFont(...) makes the menu open almost instantly, like it should (hence why I abstracted the action listener to a comment because it does not noticeably affect performance).

Edit: I added the action listener logic. As per @Hovercraft Full Of Eels's comment, a List<Font> could've worked, but users can also change the font style and size, so I cannot preload the fonts because these elements will be reset (unfortunately, Font does not have setters for the style and size). LiveAppStore is a class with some synchronized live data which is shared across a few files. MySyntaxTextArea is a subclass of JTextArea with a singleton implementation.

If it makes a difference, I'd like to note that I am writing and testing this program on my Windows laptop. On my Macbook Pro with the M1 Pro chip, the menu does load almost instantly but, of course, I cannot assume everyone has a very fast and efficient laptop. Some of my peers on Windows -- even with very high-spec computers -- notice the slight lag.

I want to ask if there's a way to overcome this, thanks in advance.

TisLeo
  • 25
  • 5
  • It doesn't make sense to make Swing calls, such as `item = new JMenuItem(s);` from within a background thread. – Hovercraft Full Of Eels Jul 06 '23 at 20:52
  • 1
    I would create a `List` so that the Font is created once and only once. – Hovercraft Full Of Eels Jul 06 '23 at 21:02
  • @HovercraftFullOfEels good thinking, but users can also change the font style and font size. If I do this, when a user clicks a font menu item, the current style and size will get reset. I'll update my question to reflect this. – TisLeo Jul 06 '23 at 22:25
  • 1
    See also the assorted `deriveFont()` methods of `Font` and `StyledEditorKit`, seen [here](https://stackoverflow.com/a/8534162/230513). – trashgod Jul 06 '23 at 23:06
  • Load the fonts via a SwingWorker - it won’t fix the loading time, it just won’t block the ui – MadProgrammer Jul 06 '23 at 23:09
  • *The problem is that the UI blocks for a second or two when the user hovers over the menu whilst its loading the font.* - not understanding that statement. The menu and its menu items would normally be created when all the other components are created. There should be no need to "load the Font", only display the menu items. Post an [mre] demonstrating your problem. Also, as a sidenote, there is no need to create a unique ActionListener for each menu item. You can create a generic listener that simply gets the Font name of the selected item and then creates the Font. – camickr Jul 06 '23 at 23:47
  • Based on my (simple) testing, it takes 3 seconds to create a list of fonts using `new Font(s, Font.PLAIN, 14)` – MadProgrammer Jul 06 '23 at 23:53
  • So, I've done a very quick test and I can build the entire font menu (192 items) without any delay under MacOS 13.4.1/Java 17 – MadProgrammer Jul 07 '23 at 00:49
  • @MadProgrammer yep, as stated it's quick on macOS but not on Windows, not 100% sure why. By load the fonts by a SwingWorker, do you mean another swing worker on top of the one I already have? – TisLeo Jul 07 '23 at 13:00
  • @camickr I want each menu item to be displayed in its respective font, so for e.g. the item with the name "Arial" would have the Arial font, like how MS Word does, which means I need to load each font. Also, thanks for the sidenote, it makes sense to have a single external listener; I'll take this into account if/when I can figure this font-loading shenanigan out. – TisLeo Jul 07 '23 at 13:05
  • Yes I understand, but the point is that the menu and menu items are built and the Font is assigned to the menu item when the GUI is created, NOT when the user hovers over the menu. You still haven't posted your [mre] so we don't know exactly what you are doing. I don't see a big delay when using Windows. If you post your MRE with just the frame and menu bar, then we can test to see if we experience the same behavour or we can comment on your approach. There should be no need to use a SwingWorker since the entire GUI needs to be created on the EDT. There is no need to extend JMenu. – camickr Jul 07 '23 at 19:12
  • @camickr I've replaced the code with a minimal reproducible example and also a GIF of what it looks like. – TisLeo Jul 07 '23 at 20:17

0 Answers0