Hover effect on a panel with children

If you are using the main C++ distribution of wxWidgets, Feel free to ask any question related to wxWidgets development here. This means questions regarding to C++ and wxWidgets, not compile problems.
Post Reply
feng
Earned a small fee
Earned a small fee
Posts: 14
Joined: Sat Jun 24, 2017 8:36 am

Hover effect on a panel with children

Post by feng »

I have trouble implementing a hover effect on a panel with children.

Here is a simplified setup:
c.png
c.png (471 Bytes) Viewed 3887 times

Code: Select all

wxPanel* panel = new wxPanel(this);
wxButton* button = new wxButton(panel, wxID_ANY, "Button");
When the mouse hovers the panel, the background color of the panel shall change,
and when the mouse leaves the panel, the color shall change back to normal:
b.png
b.png (469 Bytes) Viewed 3887 times

Code: Select all

panel->Bind(wxEVT_ENTER_WINDOW, &MyPanel::OnEnterPanel, this);
panel->Bind(wxEVT_LEAVE_WINDOW, &MyPanel::OnLeavePanel, this);

void MyPanel::OnEnterPanel(wxMouseEvent& event) {
	panel->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT));
}

void MyPanel::OnLeavePanel(wxMouseEvent& event) {
	panel->SetBackgroundColour(GetBackgroundColour());
}
When the mouse enters the button, the background shall remain highlighted:
a.png
a.png (492 Bytes) Viewed 3887 times
But, entering a child generates a leave event on the parent and an enter event on the child.
With the above code the highlighting vanishes.

How can I reliably ignore the leave event of the parent when entering children and
when I ignored it, how I can remove the highlighting when I quickly leave the
child without touching the surrounding parent?
ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 7479
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: Hover effect on a panel with children

Post by ONEEYEMAN »

Hi,
Can't you catch the enter/leave button events and just do nothing?

Thank you.
User avatar
doublemax
Moderator
Moderator
Posts: 19160
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Hover effect on a panel with children

Post by doublemax »

In the wxEVT_LEAVE_WINDOW event handler for the panel check if the mouse pointer is still inside the rectangle of the panel. If yes, ignore it.
Use the source, Luke!
PB
Part Of The Furniture
Part Of The Furniture
Posts: 4204
Joined: Sun Jan 03, 2010 5:45 pm

Re: Hover effect on a panel with children

Post by PB »

I would, perhaps naively, try
1. binding the mouse event (enter/leave) for the panel and all its children (to avoid missing a potential mouse leave event for the panel).
2. checking the (properly translated) mouse coordinates against the panel rect to see whether the mouse is inside it or not.
PB
Part Of The Furniture
Part Of The Furniture
Posts: 4204
Joined: Sun Jan 03, 2010 5:45 pm

Re: Hover effect on a panel with children

Post by PB »

Out of curiousity, I have tried the naive implementation from above

Code: Select all

#include <wx/wx.h>

class HighlightPanel : public wxPanel
{
public:
    HighlightPanel(wxWindow* parent, size_t num) : wxPanel(parent), m_mouseInside(false)
    {               
        SetName(wxString::Format("Panel%zu", num));

        wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);

        sizer->Add(new wxStaticText(this, wxID_ANY, "Static text"));
        sizer->Add(new wxTextCtrl(this, wxID_ANY));               
        sizer->Add(new wxButton(this, wxID_ANY, "Button"));       
        SetSizer(sizer);
       
        BindEnterAndLeaveWindow(this);
    }
private:
    bool m_mouseInside;

    void BindEnterAndLeaveWindow(wxWindow* p)
    {   
        p->Bind(wxEVT_ENTER_WINDOW, &HighlightPanel::OnEnterOrLeaveWindow, this);
        p->Bind(wxEVT_LEAVE_WINDOW, &HighlightPanel::OnEnterOrLeaveWindow, this);
       
        const wxWindowList& children = p->GetChildren();
        for ( wxWindowList::Node* node = children.GetFirst(); node; node = node->GetNext() )       
            BindEnterAndLeaveWindow(dynamic_cast<wxWindow*>(node->GetData()));       
    }   

    void OnEnterOrLeaveWindow(wxMouseEvent& event)
    {
        static size_t evtID = 0;
       
        const wxWindow* win = dynamic_cast<wxWindow*>(event.GetEventObject());
        const wxPoint mousePos = win->ClientToScreen(event.GetPosition());        
        const bool mouseInside = GetScreenRect().Contains(mousePos);
               
        if ( mouseInside != m_mouseInside )
        {                       
            m_mouseInside = mouseInside;
       
            if ( m_mouseInside )
                SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT));
            else
                SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_FRAMEBK));            
            Refresh();
            Update();
        }

        const wxRect winRect = win->GetScreenRect();

        wxLogMessage("Event %05zu: %s.EnterOrLeave (%s, %s), m_mouseInside = %s (mouse %d, %d; rect %d, %d, %d, %d)",
            ++evtID, GetName(), win->GetName(), event.Leaving() ? "leaving" : "entering",
            m_mouseInside ? "true" : "false", mousePos.x, mousePos.y,
            winRect.GetX(), winRect.GetY(), winRect.GetRight(), winRect.GetBottom());

        event.Skip(true); 
    }
};


class MyFrame: public wxFrame
{
public:   
    MyFrame() : wxFrame(NULL, wxID_ANY, "Test", wxDefaultPosition, wxSize(900, 600))
    {         
        wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);

        wxGridSizer* panelSizer = new wxGridSizer(3, 3, 10, 10);
        const size_t numPanels = 9;

        for ( size_t i = 0; i < numPanels; ++i )
            panelSizer->Add(new HighlightPanel(this, i + 1), 1, wxEXPAND | wxALL, 5);       
        mainSizer->Add(panelSizer, 3, wxEXPAND | wxALL, 5);

        wxTextCtrl* logCtrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH2);
        mainSizer->Add(logCtrl, 1, wxEXPAND | wxALL, 5);
        wxLog::SetActiveTarget(new wxLogTextCtrl(logCtrl)); 
        wxLog::SetTimestamp("");

        SetSizer(mainSizer);               
    }
};

class MyApp : public wxApp
{
public:         
    bool OnInit()
    {
        (new MyFrame())->Show();               
        return true;
    }   
}; wxIMPLEMENT_APP(MyApp);
I noticed that the code has a bug (I'm on Win10) where the mouse leave is not properly detected when the panel is left by (slowly) moving the mouse to the left of the wxTextCtrl. I did not have time to look into it any further.

Edit: The bug above happens because when I leave a button or label to the left, the mouse.x in client coordinates is -1 while for edit it is always 0. The edit control on Win7 seems it may be clipped by a pixel from the left but it is hard to tell... I would not be surprised if this would be somehow related to the mouse cursor changing with its position also its hotspot when going from text cursor to the arrow. Obviously, the issue could be worked around e.g. by giving a control a border so there is a space between its edges and the container edges.
feng
Earned a small fee
Earned a small fee
Posts: 14
Joined: Sat Jun 24, 2017 8:36 am

Re: Hover effect on a panel with children

Post by feng »

Thank you for the suggestions.

If there is any other window, e.g. a popup, tooltip or a window from a different application, which partly covers the panel, then entering the panel and leaving it into the overlapping window will let the panel remain highlighted.
hover-out-through-tooltip.gif
hover-out-through-tooltip.gif (59.97 KiB) Viewed 3806 times
In my first attempt to solve this issue before writing the post, I tried working with mouse positions and checking boundaries and came up with a similar solution, but at some point I must have done something differently as the panel occasionally remained highlighted when I moved the mouse quickly onto a child and out of the panel. And it was not a border issue as in your example, because my elements have been centered with a lot of space between panel and children.

Then I came up with the following attempt, that covered the flaws of the first one:
I used a common integer variable. The panel and all of its children get the OnEnter and OnLeave events and they all increase the integer on enter and decrease it on leave. Every OnEnter starts a common timer, that checks the integer every 50ms and if it is 0, the timer restores the original background color and stops itself. It works quite well and I didn't measure any extraordinary resource consumption, but still it feels wrong to do it just for a visual effect. And after all it has even worse issues with overlapping windows.

So, I guess the final solution might be to have a timer to make a conjunction between the leave event on the panel and the enter event on the child and to not just check the panel boundaries, but to check if there was an enter event on a child or if there was no enter event at all, because the mouse just left the panel by entering another window.

Actually, I was hoping that I'm just missing a flag or similar, which disables the leave event on the parent when you leave into a child, but fires the leave event when the parent is left, even when leaving quickly from children to outisde.
User avatar
doublemax
Moderator
Moderator
Posts: 19160
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Hover effect on a panel with children

Post by doublemax »

I guess you also have to recursively catch the enter/leave events from all descendants.

Here's some sample code that does that for key events:
https://wiki.wxwidgets.org/Catching_key ... ve_connect
This is an older sample that still uses Connect(), you should use Bind() instead.
Use the source, Luke!
PB
Part Of The Furniture
Part Of The Furniture
Posts: 4204
Joined: Sun Jan 03, 2010 5:45 pm

Re: Hover effect on a panel with children

Post by PB »

doublemax wrote:I guess you also have to recursively catch the enter/leave events from all descendants.
This is exactly what the code I posted above does?

The flaw of that code is that using a "mouse inside" rect does not account for windows overlapping the panel that are not its children, be it a tooltip or window belonging to another frame.

Additionally, from the brief testing I did it appears that the Enter/Leave events are not 100% reliable, sometimes I got them (or at least they were logged in a wrong order).
User avatar
doublemax
Moderator
Moderator
Posts: 19160
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Hover effect on a panel with children

Post by doublemax »

This is exactly what the code I posted above does?
Sorry, didn't see that.
Additionally, from the brief testing I did it appears that the Enter/Leave events are not 100% reliable, sometimes I got them (or at least they were logged in a wrong order).
Yes. I think for a 100% reliable solution, you'd have to put the check of the mouse position into the paint routine, which might be overkill.
Use the source, Luke!
feng
Earned a small fee
Earned a small fee
Posts: 14
Joined: Sat Jun 24, 2017 8:36 am

Re: Hover effect on a panel with children

Post by feng »

I can confirm that the enter and leave events are not always in the same order and one option to handle it is using a timer.

Here is the adjusted sample code of PB with a timer. In my tests it worked with overlapping windows, tooltips and popups. The popup requires special treatment by stopping the timer before and starting it after the popup has been shown. At first, I thought it wasn't necessary to check boundaries when working with timers, but it still is because of tooltips. Without boundary checking it would start flickering (highlight to non-highlight to highlight again) when hovering over a tooltip inside the panel .

Code: Select all

#include <wx/wx.h>

enum {
	ID_TIMER
};

class HighlightPanel : public wxPanel
{
public:
	HighlightPanel(wxWindow* parent) : wxPanel(parent), m_hoverCount(0)
	{
		static size_t panelID = 0;

		SetName(wxString::Format("Panel%zu", ++panelID));

		wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);

		sizer->Add(new wxStaticText(this, wxID_ANY, "Static text"));
		sizer->Add(new wxTextCtrl(this, wxID_ANY));
		wxButton* btn = new wxButton(this, wxID_ANY, "Button");
		btn->SetToolTip(L"Button tooltip");
		btn->Bind(wxEVT_RIGHT_UP, &HighlightPanel::OnPopup, this);
		sizer->Add(btn, wxSizerFlags().Right());
		SetSizer(sizer);

		m_timer = new wxTimer(this, ID_TIMER);
		Bind(wxEVT_TIMER, &HighlightPanel::OnTimer, this, ID_TIMER);
		BindEnterAndLeaveWindow(this);
	}

	~HighlightPanel()
	{
		delete m_timer;
	}

private:
	wxTimer* m_timer;
	int m_hoverCount;

	void BindEnterAndLeaveWindow(wxWindow* p)
	{
		p->Bind(wxEVT_ENTER_WINDOW, &HighlightPanel::OnEnter, this);
		p->Bind(wxEVT_LEAVE_WINDOW, &HighlightPanel::OnLeave, this);

		const wxWindowList& children = p->GetChildren();
		for (wxWindowList::Node* node = children.GetFirst(); node; node = node->GetNext())
			BindEnterAndLeaveWindow(dynamic_cast<wxWindow*>(node->GetData()));
	}

	void OnEnter(wxMouseEvent& event)
	{
		m_hoverCount++;
		SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT));
		Refresh();
		m_timer->Start(50);
	}

	void OnLeave(wxMouseEvent& event)
	{
		m_hoverCount--;
	}

	void OnTimer(wxTimerEvent& event)
	{
		if (m_hoverCount == 0)
		{
			const wxRect rect = GetClientRect();
			wxPoint mousePos = ScreenToClient(wxGetMousePosition());
			bool mouseInside = rect.Contains(mousePos);

			if (!mouseInside)
			{
				m_timer->Stop();
				SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_FRAMEBK));
				Refresh();
			}			
		}
	}

	void OnPopup(wxMouseEvent& event)
	{
		m_timer->Stop();
		wxMenu menu;
		menu.Append(0, "Some context action 1");
		menu.Append(1, "Some context action 2");
		menu.Append(2, "Some context action 3");
		PopupMenu(&menu);
		m_timer->Start();
	}
};


class MyFrame : public wxFrame
{
public:
	MyFrame() : wxFrame(NULL, wxID_ANY, "Test", wxDefaultPosition, wxSize(600, 600))
	{
		wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);

		wxGridSizer* panelSizer = new wxGridSizer(3, 3, 10, 10);
		const size_t numPanels = 9;

		for (size_t i = 0; i < numPanels; ++i)
			panelSizer->Add(new HighlightPanel(this), 1, wxEXPAND | wxALL, 5);
		mainSizer->Add(panelSizer, 3, wxEXPAND | wxALL, 5);

		wxTextCtrl* logCtrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH2);
		mainSizer->Add(logCtrl, 1, wxEXPAND | wxALL, 5);
		wxLog::SetActiveTarget(new wxLogTextCtrl(logCtrl));

		SetSizer(mainSizer);
	}
};

class MyApp : public wxApp
{
public:
	bool OnInit()
	{
		(new MyFrame())->Show();
		return true;
	}
}; wxIMPLEMENT_APP(MyApp);
Thank you very much for helping me out.
gunterkoenigsmann
Earned a small fee
Earned a small fee
Posts: 14
Joined: Tue May 31, 2016 11:30 am

Re: Hover effect on a panel with children

Post by gunterkoenigsmann »

Found a solution:

Each element and child element will issue wxEVT_ENTER_WINDOW and wxEVT_LEAVE_WINDOW signals if the mouse pointer enters and leaves the window so the application can maintain a flag for each element telling if the mouse pointer is inside this wxWindow.

These signals might arrive more or less in any order. But what we know is that an wxEVT_IDLE arrives only when the last of these signals has arrived. So the Right Place to evaluate the flags that tell where the mouse pointer currently resides is the idle event.
ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 7479
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: Hover effect on a panel with children

Post by ONEEYEMAN »

Hi,
The problem here is the window hierarchy.

Window controls has to be created with the parent and so you have to distinguish where the cursor is - over the parent or the child.

Thank you.
Post Reply