Search code examples
c++macosuser-interfacecomboboxwxwidgets

wxOwnerDrawnComboBox on OSX has an extra row at the bottom of the scroll


I've implemented a custom wxOwnerDrawnComboBox based on the API docs. I then placed it on a wxPanel then used SetPopupMaxHeight to limit the combo popup to show only 3 items at a time. But the popup shows an extra white row at the bottom of the popup window.

Here are screenshots (I implemented it on the minimal_cocoa sample project):
pic1 pic2

As shown, there are only 6 items but at the end of "item 5", there's an extra row.

Some notes on the issue:

  • It is always the default white (checked by customizing the bg of the other rows to black)
  • It only happens when using SetPopupMaxHeight
  • It only happens on Mac (i.e. no issue on Windows)
  • Clicking on that extra row will close the popup (so I guess it's drawn part of the popup window)

Here is the code (simplified for the minimal_cocoa project):


// CUSTOM CLASS DECLARATION

class MyOwnerDrawnComboBox : public wxOwnerDrawnComboBox
{
public:
    static int getItemHeight()
    {
        return 25;
    }

public:
    // ctor

    MyOwnerDrawnComboBox(wxWindow* parent,
                         const wxPoint& pos = wxDefaultPosition,
                         const wxSize& size = wxDefaultSize,
                         int n = 0,
                         const wxString choices[] = NULL,
                         long style = wxODCB_STD_CONTROL_PAINT)
    : wxOwnerDrawnComboBox(parent,
                           wxID_ANY,
                           wxEmptyString,
                           pos,
                           size,
                           n,
                           choices,
                           style,
                           wxDefaultValidator,
                           "MyOwnerDrawnComboBox")
    {
    }

    // overrides

    virtual void OnDrawItem (wxDC& dc, const wxRect& rect, int item, int flags) const
    {
        dc.SetTextForeground(*wxRED);

        // for debugging purposes, display the item index instead
        wxString label = wxString::Format("item %d", item);
        dc.DrawLabel(label, rect, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
    }

    virtual wxCoord OnMeasureItem (size_t item) const
    {
        return 25;
    }

    virtual wxCoord OnMeasureItemWidth (size_t item) const
    {
        return -1; // use default
    }
};

// CUSTOM CLASS USAGE

wxPanel* panel = new wxPanel(this, wxID_ANY);
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
wxString items[] = {"1", "2", "3", "4", "5", "6"}; // unused
MyOwnerDrawnComboBox* odcb = new MyOwnerDrawnComboBox(panel,
                                                      wxDefaultPosition,
                                                      wxSize(150, 30),
                                                      6,
                                                      items);
odcb->SetPopupMaxHeight(3 * MyOwnerDrawnComboBox::getItemHeight());
sizer->Add(odcb, 0, wxALL, 10);
sizer->Layout();
panel->SetSizer(sizer);

I've tried a few things:

  1. Used the default OnDrawItem method (same issue)
  2. Checked for extra calls to OnDrawItem (no extra calls)
  3. Adjusted the returned item height in OnMeasureHeight (same issue)
  4. Checked the behavior on the samples/combo/combo.cpp sample (same issue)

    combo-cocoa-1 combo-cocoa-2


Some info on my wxWidgets build:

  • Cloned it directly from their github repo
  • Made no changes to the wxWidgets source aside from defining wxUSE_XTEST 1 in setup.h
    • I checked and wxUSE_ODCOMBOBOX and wxUSE_COMBOCTRL are #define'd
  • Used the following options for ../configure
    • --with-osx_cocoa
    • --with-macosx-version-min=10.7
    • --with-macosx-sdk=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk
    • --enable-debug
    • --disable-shared
    • --enable-unicode
    • --prefix="$(pwd)"

Some info on my env:

  • macOS 10.12.5 (Sierra)
  • Xcode 8.3.3 (8E3004b)
  • minimal_cocoa displays wxWidgets 3.1.1

Has anyone encountered this issue before?
I'm working around it for now by not using SetPopupMaxHeight anymore.

If someone has a solution though, do share.


Solution

  • I fixed it by doing some changes to the wx source.

    src/generic/vscroll.cpp

    wxVarScrollHelperBase::DoScrollToUnit:

    // determine the real first unit to scroll to: we shouldn't scroll beyond
    // the end
    size_t unitFirstLast = FindFirstVisibleFromLast(m_unitMax - 1, true);
    if ( unit > unitFirstLast )
        unit = unitFirstLast;
    

    Pass false instead to FindFirstVisibleFromLast.

    This will allow the method to consider an item as the first item even though the last item is only partially visible ("partially" here, based on my testing, is really like "almost fully visible", with only about ~2pts that are hidden). With true, the scroll moves down to the next item, thereby adding the extra row.

    src/generic/odcombo.cpp

    wxVListBoxComboPopup::OnMouseMove:

            // Only change selection if item is fully visible
            if ( (y + fromBottom) >= 0 )
            {
                wxVListBox::SetSelection((int)line);
                return;
            }
    

    To match the 2pt borders being considered in wxVListBoxComboPopup::GetAdjustedSize, I changed the IF condition to (y + fromBottom + 2) to allow moving the mouse over the last item in the scrolled window, even though it isn't fully visible.

    src/generic/vlbox.cpp

        else // line is at least partly visible
        {
            // it is, indeed, only partly visible, so scroll it into view to
            // make it entirely visible
            // BUT scrolling down when m_current is first visible makes it
            // completely hidden, so that is even worse
            while ( (size_t)m_current + 1 == GetVisibleRowsEnd() &&
                    (size_t)m_current != GetVisibleRowsBegin() &&
                    ScrollToRow(GetVisibleBegin() + 1) ) ;
    

    Comment-out that while-loop to disable auto-scrolling when the mouse pointer is over the partially visible item.