ImageButton Repainting Premature/Timing?


#1

I’m not sure if this is the correct behavior or if I’m implementing something wrong. I haven’t ever noticed something like this so it kind of jumped out at me.

Testing in macOS 10.12 in a VST Audio Plugin.

In Component A I have two child Components, button & B. button is an ImageButton, and when it is clicked it sets B to visible. B is the same size as A & paints with a solid black color. A paints a solid white color.

The issue I’m running into is that when button is clicked, sometimes the bounds of button paints B’s black color before the entire bounds of B is updated to black. Below shows a screen recording, frame by frame:

17 PM
2.
21 PM
3.
37 PM

It seems like B paints the clipped bounds of button before B paints its entire self?

Many thanks!


#2

fyi attaching an OpenGLContext to the top level Component seems to fix this issue.

I was mistaken. It merely makes the effect less noticeable on most hosts. It’s still very easy to see on WaveLab Pro 9.


#3

Ok, this one has actually been quite tricky for me. Can anybody else reproduce this behavior?

  1. Boot into macOS (I’m running 10.12.3)
  2. Checkout the tip on the develop branch.
  3. Using the Projucer, make a new Audio Plugin project.
  4. Delete the code in the PluginEditor.cpp file.
  5. In the PluginEditor.h file, copy in this code (& change the class name if needed):
#pragma once

#include "../JuceLibraryCode/JuceHeader.h"
#include "PluginProcessor.h"

class Overlay : public Component
{
public:
    Overlay() {}
    void paint (Graphics& g) override { g.fillAll (Colours::black); }
    void mouseUp (const MouseEvent& event) override { setVisible (false); }
};

class ButtonChangesBackgroundPluginAudioProcessorEditor : public AudioProcessorEditor, public Button::Listener
{
public:
    ButtonChangesBackgroundPluginAudioProcessorEditor (ButtonChangesBackgroundPluginAudioProcessor& p)
    : AudioProcessorEditor (&p)
    {
        setSize (600, 400);

        button.setButtonText ("Press Me");
        button.addListener (this);
        addAndMakeVisible (&button);

        addChildComponent (&overlay);
    }

    ~ButtonChangesBackgroundPluginAudioProcessorEditor() {}

    void paint (Graphics& g) override { g.fillAll (Colours::white); }

    void resized() override
    {
        overlay.setBounds (getLocalBounds ());
        button.setBounds (200, 100, 200, 50);
    }

    void buttonClicked (Button* button) override { overlay.setVisible (true); }

private:
    Overlay overlay;
    TextButton button;
};
  1. To test, click the “Click Me” button. The bounds of the button will turn black before the entire GUI turns black. Use QuickTime Player to make a screen recording, where you can then inch forward frame by frame to see the full effect. This “glitchyness” is more noticeable in certain environments with certain graphics/images used. Click on the black background to return to the white background to retest.

• This seems to only happen on macOS. Windows seems fine.
• The issue also occurs with ImageButton & DrawableButton. I haven’t tested any other buttons.
• I’ve tried a regular GUI App project, & this issue doesn’t seem to occur.
• I haven’t done extensive testing on this, but it seems like the more the event thread is cluttered, the more lag there is between the clipped black and the full back background updating, which makes the glitchyness a lot more jarring.
• So far I’ve tested on Reaper64, Logic Pro X, WaveLab Pro 9, Cubase Pro 9, Ableton Live 9 & this issue exists on all.

I’m no event thread paint timing guru, but it seems like the bounds of the button is marked dirty. Those bounds are then repainted but using the newly visible Overlay's paint method. I’m currently using mini AsyncUpdater objects to delay setting the overlay to visible, but this is very much a temp hack workaround fix for the meantime.

Any help would be much appreciated! :smile:


#4

Investigating your code sample, it looks like this is due to unlucky timing of repaint calls.

Basically, when the button receives it’s mouseUp event it will also mark the region of the button as dirty. When doing this, the OS can decide to schedule a paint event (for that region) or wait a bit longer and hope for more regions to become dirty so that it can coalesce several dirty regions. In this case, macOS seems to immediately schedule a paint event.

Then your button listener is called: you set the overlay to be visible which now marks the entire area as dirty. However, macOS has already scheduled the first paint event so it needs to schedule another one now.

When the first paint event arrives, it will only be for the region of the button. As the overlay has already been marked as visible, repainting the region of the button will result in a black box. Then the second paint event arrives, repainting the whole region. If the OS chooses to split paint events like this then there is not much that JUCE can do.

One way to workaround the problem is to create your own simple button class which will call the button listener before it calls repaint. Something like this maybe:

class MySimpleButton : public Component
{
public:
    MySimpleButton()
    {
        setInterceptsMouseClicks (true, false);
    }


    void setListener (Button::Listener* l)    { listener = l; }

    void paint (Graphics& g) override
    {
        g.setColour (isDown ? Colours::blue : Colours::lightblue);
        g.fillRoundedRectangle (getLocalBounds().toFloat(), 80.0f);
        g.setColour (Colours::black);
        g.drawText ("Press Me", getLocalBounds(), Justification::centred);
    }

    void mouseDown (const MouseEvent&) override
    {
        isDown = true;
        repaint();
    }

    void mouseUp (const MouseEvent&) override
    {
        isDown = false;
        if (listener != nullptr)
            listener->buttonClicked (nullptr);

        repaint();
    }

private:
    bool isDown = false;
    Button::Listener* listener = nullptr;
};

#5

Hi Fabian, thanks so much for taking a look at this. I tried your MySimpleButton workaround and it seems to work great! The only drawback is that I would have to roll my own versions of Image/Text/etc… buttons. Plus some of my buttons attach to my processor via ButtonAttachments. I did try poking around the JUCE Button class to try to see if there was an easy fix in there as well, but didn’t get any immediate low-hanging-fruit results.

Do you think this is a behavior that will ultimately be assessed within the JUCE Button class?

Many thanks! :slight_smile:


#6

Hi @RustyPine,

Before we start messing around in the JUCE button class, you may want to try one more thing. When JUCE is run as an audio plugin, we internally mess around with the coalescing of dirty rectangles to workaround a macOS El Capitan bug (see this, this and this thread).

Try disabling the workaround by changing line juce_mac_NSViewComponentPeer.mm:917 to:

    static bool shouldThrottleRepaint()
    {
        return false;
    }

Disabling this workaround should be ok if your plugin is not doing heavy repainting like 60fps animations or similar.

Let me know if this works for you. If yes, then we’ll think about ways to make the El Capitan workaround optional.


#7

Ah, that works fantastically! Luckily the current plugin I’m working on does not require any high FPS painting. I suppose it would indeed be nice to make this optional.

Many thanks! :smiley:


#8

Thanks for looking at that and for the details RustyPine & Fabian.
That also explains those buttons repaint glitches:



#9

Maybe this could a juce_gui_graphics config flag which can be set in the Projucer. What do people think?


#10

Seems like a good solution to me.


#11

tbh I haven’t been through all those threads yet, so I’m not sure about the implications.
But aren’t there any cases where people would want shouldThrottleRepaint() to return true and then still need that button repaint issue to be fixed somehow?


#12

I guess you did not have the time to look more at that issue Fabian?
do you think there is a way to fix that button glitch/repaint somehow, or the only way is to turn off shouldThrottleRepaint?


#13

Yes I think that’s the only way to do this.


#14

ok, then your suggestion to set it as a projucer config flag sounds like a good option :slight_smile:


#15

I think this is quite specific to your use case. As I mentioned above, even disabling the paint throttle workaround will not guarantee to fix your problem - this is a general problem with your code as it relies on the OS sending and receiving paint events in a certain order.

I’ve posted a proper workaround for this above which involves rolling your own custom button - it could even derive from the button class.