0

I'm experimenting with javax.swing. Here's a simple task I was going to do: to make a circle change color once a button (a sibling component) is pressed. Here's my code. I'm enthusiastic about design patterns so you see all these singletons, builders, and whatnot (I hope you don't mind)

public class App {
    public static void main(String[] args) {
        MyUI ui = new MyUI();
        ui.display();
    }
}
public class MyUI {
    public void display() {
        UIUtil.getUIBuilder()
                .withButton(SOUTH, () -> {
                    JButton button = new JButton("click me!");
                    button.setFont(new Font(SANS_SERIF, BOLD, 28));
                    button.setSize(150, 50);
                    button.addMouseListener(MouseListeningPanel.getInstance());
                    return button;
                })
                .withPanel(CENTER, MouseListeningPanel::getInstance)
                .withFrameSize(300, 300)
                .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
                .visualize();
    }
}
public class MouseListeningPanel extends JPanel implements MouseListener {
    public static final MouseListeningPanel INSTANCE = new MouseListeningPanel();
    private Color startColor = UIUtil.getRandomColor();
    private Color endColor = UIUtil.getRandomColor();
    private MouseListeningPanel() {}
    public static MouseListeningPanel getInstance() {
        return INSTANCE;
    }

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D twoDGraphics = (Graphics2D) g;
        GradientPaint gradient = new GradientPaint(0, 0, startColor, getMiddle(), getMiddle(), endColor);
        twoDGraphics.setPaint(gradient);
        twoDGraphics.fillOval(calculateX(), calculateY(), calculateWidth(), calculateWidth());
    }

    private float getMiddle() {
        return getWidth() / 2f;
    }

    private int calculateX() {
        return getWidth() / 2 - calculateWidth() / 2;
    }

    private int calculateY() {
        return getHeight() / 2 - calculateWidth() / 2;
    }

    private int calculateWidth() {
        return Math.min(getWidth(), getHeight()) / 2;
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        startColor = UIUtil.getRandomColor();
        endColor = UIUtil.getRandomColor();
        repaint();
    }

    // dummy implementations for other MouseListener methods
    // I can't extend JPanel and MouseAdapter at the same time so I have to provide them
}
public class UIUtil {

    public static UIBuilder getUIBuilder() {
        return new UIBuilder();
    }
    public static UIBuilder getUIBuilder(Supplier<Container> contentPaneSupplier) {
        return new UIBuilder(contentPaneSupplier);
    }
    public static Color getRandomColor() {
        int red = Util.randomInt(255);
        int green = Util.randomInt(255);
        int blue = Util.randomInt(255);
        return new Color(red, green, blue);
    }

    public static SimpleRectangle getRandomRectangle(int parentWidth, int parentHeight) {
        return getRandomRectangle(parentWidth, parentHeight, 0.5F);
    }

    public static SimpleRectangle getRandomRectangle(int parentWidth, int parentHeight, float recDimensionToParentDimensionRatio) {
        int x = Util.randomInt(parentWidth);
        int y = Util.randomInt(parentHeight);
        int width = Util.randomInt((int) (parentWidth * recDimensionToParentDimensionRatio));
        int height = Util.randomInt((int) (parentHeight * recDimensionToParentDimensionRatio));
        return new SimpleRectangle(x, y, width, height);
    }

    public record SimpleRectangle(int x, int y, int width, int height) {}

    @NoArgsConstructor
    public static class UIBuilder {
        private final JFrame frame;
        private final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

        {
            frame = new JFrame();
            this.withFrameSize(300, 300)
            .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        }

        public UIBuilder(@NotNull Supplier<Container> contentPaneSupplier) {
            Objects.requireNonNull(contentPaneSupplier);
            frame.setContentPane(contentPaneSupplier.get());
        }

        public UIBuilder withButton(@NotNull String position, @NotNull Supplier<JButton> buttonSupplier) {
            Stream.of(position, buttonSupplier).forEach(Objects::requireNonNull);
            JButton button = buttonSupplier.get();
            frame.getContentPane().add(position, button);
            return this;
        }

        public UIBuilder withPanel(@NotNull String position, @NotNull Supplier<JPanel> panelSupplier) {
            Stream.of(position, panelSupplier).forEach(Objects::requireNonNull);
            JPanel panel = panelSupplier.get();
            JScrollPane scrollPane = new JScrollPane(panel);
            frame.getContentPane().add(position, scrollPane);
            return this;
        }

        public UIBuilder withFrameSize(int width, int height) {
            checkAgainstScreenSize(width, height);
            int x = (screenSize.width - width) / 2;
            int y = (screenSize.height - height) / 2;
            frame.setBounds(x, y, width, height);
            return this;
        }

        private void checkAgainstScreenSize(int width, int height) {
            if (screenSize.width < width) {
                throw new IllegalArgumentException("Frame width cannot be greater than screen width");
            } else if (screenSize.height < height) {
                throw new IllegalArgumentException("Frame height cannot be greater than screen height");
            }
        }

        public UIBuilder withDefaultCloseOperation(int windowConstant) {
            frame.setDefaultCloseOperation(windowConstant);
            return this;
        }

        public void visualize() {
            frame.setVisible(true);
        }
    }
}

It kind of works, but here's the snag: once I press the button, it gets cloned, and the copy is put in the NORTH section of the pane. I can't imagine why Java did that

enter image description here

Since regular resizing doesn't produce that result and mouseClicked() only sets colors and calls repaint(), repaint() is the only suspect. I checked the doc for the method

Repaints this component

This! The button is not "this", is it? The button is a sibling, not a component of the panel. "This" must refer to the panel itself, and its paintComponent() doesn't add any buttons

Why is it happening and how do I fix it?

JoreJoh
  • 113
  • 5
  • 1
    You'll probably need a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) – g00se Aug 06 '23 at 21:57
  • 4
    Don't forget to add `super.paintComponent(...)` as the first statement in your painting method to make sure the background is cleared. – camickr Aug 06 '23 at 22:10
  • Ah `button.setSize(150, 50);` no and `frame.setBounds(x, y, width, height);` no and actually `Toolkit.getDefaultToolkit().getScreenSize();` is erroneous when determining the viewable area of the screen (task bars any one?) and `pack` and `setLocationRelativeTo(null)` will actually do what you're trying to do ... just better – MadProgrammer Aug 06 '23 at 23:54
  • @MadProgrammer sorry, but I have no idea what you just said – JoreJoh Aug 07 '23 at 00:39
  • @camickr thank you! But how is the lack of that statement connected to that button issue? – JoreJoh Aug 07 '23 at 00:43
  • 1
    I already stated: *to make sure the background is cleared.* Swing painting is complicated. When a component is opaque it is guaranteed to paint the background first. By forgetting that statement the background not cleared. I don't know the internals of why the button would be painted twice but I do know when you don't clear the background you can get painting artifacts. – camickr Aug 07 '23 at 00:47
  • @JoreJoh `JFrame@pack` will pack the frame decorations around the content, so that the view area of the content will be based on their `preferredSize`. `JFrame#setLocationRelativeTo(null)` will centre position the window (on the default screen), taking into consideration the things like the task bar (on Windows) and the dock and menu bar on MacOS - unlike the solution you've been trying to hobble together – MadProgrammer Aug 07 '23 at 06:33

0 Answers0