/////////////////////////////////////////////////////////////////////////////
// Name:        src/msw/bmpcbox.cpp
// Purpose:     wxBitmapComboBox
// Author:      Jaakko Salli
// Created:     2008-04-06
// Copyright:   (c) 2008 Jaakko Salli
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////
 
// ============================================================================
// declarations
// ============================================================================
 
// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------
 
#include "wx/wxprec.h"
 
#ifdef __BORLANDC__
    #pragma hdrstop
#endif
 
#if wxUSE_BITMAPCOMBOBOX
 
#include "wx/bmpcbox.h"
 
#ifndef WX_PRECOMP
    #include "wx/log.h"
#endif
 
#include "wx/settings.h"
#include "wx/vector.h"
 
#include "wx/msw/dcclient.h"
#include "wx/msw/private.h"
 
// For wxODCB_XXX flags
#include "wx/odcombo.h"
 
 
#define IMAGE_SPACING_CTRL_VERTICAL 7  // Spacing used in control size calculation
 
 
// ============================================================================
// implementation
// ============================================================================
 
 
wxBEGIN_EVENT_TABLE(wxBitmapComboBox, wxComboBox)
    EVT_SIZE(wxBitmapComboBox::OnSize)
wxEND_EVENT_TABLE()
 
 
wxIMPLEMENT_DYNAMIC_CLASS(wxBitmapComboBox, wxComboBox);
 
 
// ----------------------------------------------------------------------------
// wxBitmapComboBox creation
// ----------------------------------------------------------------------------
 
void wxBitmapComboBox::Init()
{
    m_inResize = false;
}
 
wxBitmapComboBox::wxBitmapComboBox(wxWindow *parent,
                                  wxWindowID id,
                                  const wxString& value,
                                  const wxPoint& pos,
                                  const wxSize& size,
                                  const wxArrayString& choices,
                                  long style,
                                  const wxValidator& validator,
                                  const wxString& name)
    : wxComboBox(),
      wxBitmapComboBoxBase()
{
    Init();
 
    Create(parent,id,value,pos,size,choices,style,validator,name);
}
 
bool wxBitmapComboBox::Create(wxWindow *parent,
                              wxWindowID id,
                              const wxString& value,
                              const wxPoint& pos,
                              const wxSize& size,
                              const wxArrayString& choices,
                              long style,
                              const wxValidator& validator,
                              const wxString& name)
{
    wxCArrayString chs(choices);
    return Create(parent, id, value, pos, size, chs.GetCount(),
                  chs.GetStrings(), style, validator, name);
}
 
bool wxBitmapComboBox::Create(wxWindow *parent,
                              wxWindowID id,
                              const wxString& value,
                              const wxPoint& pos,
                              const wxSize& size,
                              int n,
                              const wxString choices[],
                              long style,
                              const wxValidator& validator,
                              const wxString& name)
{
    if ( !wxComboBox::Create(parent, id, value, pos, size,
                             n, choices, style, validator, name) )
        return false;
 
    UpdateInternals();
 
    return true;
}
 
WXDWORD wxBitmapComboBox::MSWGetStyle(long style, WXDWORD *exstyle) const
{
    return wxComboBox::MSWGetStyle(style, exstyle) | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS;
}
 
void wxBitmapComboBox::RecreateControl()
{
    //
    // Recreate control so that WM_MEASUREITEM gets called again.
    // Can't use CBS_OWNERDRAWVARIABLE because it has odd
    // mouse-wheel behaviour.
    //
    wxString value = GetValue();
    wxPoint pos = GetPosition();
    wxSize size = GetSize();
    size.y = GetBestSize().y;
    const wxArrayString strings = GetStrings();
    const unsigned numItems = strings.size();
    unsigned i;
 
    // Save the client data pointers before clearing the control, if any.
    const wxClientDataType clientDataType = GetClientDataType();
    wxVector<wxClientData*> objectClientData;
    wxVector<void*> voidClientData;
    switch ( clientDataType )
    {
        case wxClientData_None:
            break;
 
        case wxClientData_Object:
            objectClientData.reserve(numItems);
            for ( i = 0; i < numItems; ++i )
                objectClientData.push_back(GetClientObject(i));
            break;
 
        case wxClientData_Void:
            voidClientData.reserve(numItems);
            for ( i = 0; i < numItems; ++i )
                voidClientData.push_back(GetClientData(i));
            break;
    }
 
    wxComboBox::DoClear();
 
    HWND hwnd = GetHwnd();
    DissociateHandle();
    ::DestroyWindow(hwnd);
 
    if ( !MSWCreateControl(wxT("COMBOBOX"), wxEmptyString, pos, size) )
        return;
 
    // initialize the controls contents
    for ( i = 0; i < numItems; i++ )
    {
        wxComboBox::Append(strings[i]);
 
        if ( !objectClientData.empty() )
            SetClientObject(i, objectClientData[i]);
        else if ( !voidClientData.empty() )
            SetClientData(i, voidClientData[i]);
    }
 
    // and make sure it has the same attributes as before
    if ( m_hasFont )
    {
        // calling SetFont(m_font) would do nothing as the code would
        // notice that the font didn't change, so force it to believe
        // that it did
        wxFont font = m_font;
        m_font = wxNullFont;
        SetFont(font);
    }
 
    if ( m_hasFgCol )
    {
        wxColour colFg = m_foregroundColour;
        m_foregroundColour = wxNullColour;
        SetForegroundColour(colFg);
    }
 
    if ( m_hasBgCol )
    {
        wxColour colBg = m_backgroundColour;
        m_backgroundColour = wxNullColour;
        SetBackgroundColour(colBg);
    }
    else
    {
        SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
    }
 
    ::SendMessage(GetHwnd(), CB_SETITEMHEIGHT, 0, MeasureItem(0));
 
    // Revert the old string value
    if ( !HasFlag(wxCB_READONLY) )
        ChangeValue(value);
 
    // If disabled we'll have to disable it again after re-creating
    if ( !IsEnabled() )
        DoEnable(false);
}
 
wxBitmapComboBox::~wxBitmapComboBox()
{
    Clear();
}
 
wxSize wxBitmapComboBox::DoGetBestSize() const
{
    wxSize best = wxComboBox::DoGetBestSize();
    wxSize bitmapSize = GetBitmapSize();
 
    wxCoord useHeightBitmap = EDIT_HEIGHT_FROM_CHAR_HEIGHT(bitmapSize.y);
    if ( best.y < useHeightBitmap )
        best.y = useHeightBitmap;
    return best;
}
 
// ----------------------------------------------------------------------------
// Item manipulation
// ----------------------------------------------------------------------------
 
void wxBitmapComboBox::SetItemBitmap(unsigned int n, const wxBitmap& bitmap)
{
    OnAddBitmap(bitmap);
    DoSetItemBitmap(n, bitmap);
 
    if ( (int)n == GetSelection() )
        Refresh();
}
 
int wxBitmapComboBox::Append(const wxString& item, const wxBitmap& bitmap)
{
    OnAddBitmap(bitmap);
    const int n = wxComboBox::Append(item);
    if ( n != wxNOT_FOUND )
        DoSetItemBitmap(n, bitmap);
    return n;
}
 
int wxBitmapComboBox::Append(const wxString& item, const wxBitmap& bitmap,
                             void *clientData)
{
    OnAddBitmap(bitmap);
    const int n = wxComboBox::Append(item, clientData);
    if ( n != wxNOT_FOUND )
        DoSetItemBitmap(n, bitmap);
    return n;
}
 
int wxBitmapComboBox::Append(const wxString& item, const wxBitmap& bitmap,
                             wxClientData *clientData)
{
    OnAddBitmap(bitmap);
    const int n = wxComboBox::Append(item, clientData);
    if ( n != wxNOT_FOUND )
        DoSetItemBitmap(n, bitmap);
    return n;
}
 
int wxBitmapComboBox::Insert(const wxString& item,
                             const wxBitmap& bitmap,
                             unsigned int pos)
{
    OnAddBitmap(bitmap);
    const int n = wxComboBox::Insert(item, pos);
    if ( n != wxNOT_FOUND )
        DoSetItemBitmap(n, bitmap);
    return n;
}
 
int wxBitmapComboBox::Insert(const wxString& item, const wxBitmap& bitmap,
                             unsigned int pos, void *clientData)
{
    OnAddBitmap(bitmap);
    const int n = wxComboBox::Insert(item, pos, clientData);
    if ( n != wxNOT_FOUND )
        DoSetItemBitmap(n, bitmap);
    return n;
}
 
int wxBitmapComboBox::Insert(const wxString& item, const wxBitmap& bitmap,
                             unsigned int pos, wxClientData *clientData)
{
    OnAddBitmap(bitmap);
    const int n = wxComboBox::Insert(item, pos, clientData);
    if ( n != wxNOT_FOUND )
        DoSetItemBitmap(n, bitmap);
    return n;
}
 
int wxBitmapComboBox::DoInsertItems(const wxArrayStringsAdapter & items,
                                    unsigned int pos,
                                    void **clientData, wxClientDataType type)
{
    const unsigned int numItems = items.GetCount();
 
    int index;
    if ( HasFlag(wxCB_SORT) )
    {
        // Since we don't know at what positions new elements will be actually inserted
        // we need to add them one by one, check for each one the position it was added at
        // and reserve the slot for corresponding bitmap at the same postion in the bitmap array.
        index = pos;
        for ( unsigned int i = 0; i < numItems; i++ )
        {
            if ( clientData )
                index = wxComboBox::DoInsertItems(items[i], pos+i, clientData+i, type);
            else
                index = wxComboBox::DoInsertItems(items[i], pos+i, NULL, wxClientData_None);
 
            wxASSERT_MSG( index != wxNOT_FOUND, wxS("Invalid wxBitmapComboBox state") );
            if ( index == wxNOT_FOUND )
            {
                continue;
            }
 
            // Update the bitmap array.
            if ( GetCount() > m_bitmaps.Count() )
            {
                wxASSERT_MSG( GetCount() == m_bitmaps.Count() + 1,
                              wxS("Invalid wxBitmapComboBox state") );
                // Control is in the normal state.
                // New item has been just added.
                // Insert bitmap at the given index into the array.
                wxASSERT_MSG( (size_t)index <= m_bitmaps.Count(),
                              wxS("wxBitmapComboBox item index out of bound") );
                m_bitmaps.Insert(new wxBitmap(wxNullBitmap), index);
            }
            else
            {
                // No. of items after insertion <= No. bitmaps:
                // (This can happen if control is e.g. recreated with RecreateControl).
                // In this case existing bitmaps are reused.
                // Required and actual indices should be the same to assure
                // consistency between list of items and bitmap array.
                wxASSERT_MSG( (size_t)index < m_bitmaps.Count(),
                              wxS("wxBitmapComboBox item index out of bound") );
                wxASSERT_MSG( (unsigned int)index == pos+i,
                              wxS("Invalid index for wxBitmapComboBox item") );
            }
        }
    }
    else
    {
        if ( GetCount() == m_bitmaps.Count() )
        {
            // Control is in the normal state.
            // Just insert new bitmaps into the array.
            const unsigned int countNew = GetCount() + numItems;
            m_bitmaps.Alloc(countNew);
 
            for ( unsigned int i = 0; i < numItems; i++ )
            {
                m_bitmaps.Insert(new wxBitmap(wxNullBitmap), pos + i);
            }
        }
        else
        {
            wxASSERT_MSG( GetCount() < m_bitmaps.Count(),
                          wxS("Invalid wxBitmapComboBox state") );
            // There are less items then bitmaps.
            // (This can happen if control is e.g. recreated with RecreateControl).
            // In this case existing bitmaps are reused.
            // The whole block of inserted items should be within the range
            // of indices of the existing bitmap array.
            wxASSERT_MSG( pos + numItems <= m_bitmaps.Count(),
                      wxS("wxBitmapComboBox item index out of bound") );
        }
 
        index = wxComboBox::DoInsertItems(items, pos,
                                          clientData, type);
        // This returns index of the last item in the inserted block.
 
        if ( index == wxNOT_FOUND )
        {
            for ( int i = numItems-1; i >= 0; i-- )
                BCBDoDeleteOneItem(pos + i);
        }
        else
        {
            // Index of the last inserted item should be consistent
            // with required position and number of items.
            wxASSERT_MSG( (unsigned int)index == pos+numItems-1,
                           wxS("Invalid index for wxBitmapComboBox item") );
        }
    }
 
    return index;
}
 
bool wxBitmapComboBox::OnAddBitmap(const wxBitmap& bitmap)
{
    if ( wxBitmapComboBoxBase::OnAddBitmap(bitmap) )
    {
        // Need to recreate control for a new measureitem call?
        int prevItemHeight = ::SendMessage(GetHwnd(), CB_GETITEMHEIGHT, 0, 0);
 
        if ( prevItemHeight != MeasureItem(0) )
            RecreateControl();
 
        return true;
    }
 
    return false;
}
 
void wxBitmapComboBox::DoClear()
{
    wxComboBox::DoClear();
    wxBitmapComboBoxBase::BCBDoClear();
}
 
void wxBitmapComboBox::DoDeleteOneItem(unsigned int n)
{
    wxComboBox::DoDeleteOneItem(n);
    wxBitmapComboBoxBase::BCBDoDeleteOneItem(n);
}
 
// ----------------------------------------------------------------------------
// wxBitmapComboBox event handlers and such
// ----------------------------------------------------------------------------
 
void wxBitmapComboBox::OnSize(wxSizeEvent& event)
{
    // Prevent infinite looping
    if ( !m_inResize )
    {
        m_inResize = true;
        DetermineIndent();
        m_inResize = false;
    }
 
    event.Skip();
}
 
// ----------------------------------------------------------------------------
// wxBitmapComboBox miscellaneous
// ----------------------------------------------------------------------------
 
bool wxBitmapComboBox::SetFont(const wxFont& font)
{
    bool res = wxComboBox::SetFont(font);
    UpdateInternals();
    return res;
}
 
// ----------------------------------------------------------------------------
// wxBitmapComboBox item drawing and measuring
// ----------------------------------------------------------------------------
 
bool wxBitmapComboBox::MSWOnDraw(WXDRAWITEMSTRUCT *item)
{
    LPDRAWITEMSTRUCT lpDrawItem = (LPDRAWITEMSTRUCT) item;
    int pos = lpDrawItem->itemID;
 
    // Draw default for item -1, which means 'focus rect only'
    if ( pos == -1 )
        return false;
 
    int flags = 0;
    if ( lpDrawItem->itemState & ODS_COMBOBOXEDIT )
        flags |= wxODCB_PAINTING_CONTROL;
    if ( lpDrawItem->itemState & ODS_SELECTED )
        flags |= wxODCB_PAINTING_SELECTED;
 
    wxPaintDCEx dc(this, lpDrawItem->hDC);
    wxRect rect = wxRectFromRECT(lpDrawItem->rcItem);
    wxBitmapComboBoxBase::DrawBackground(dc, rect, pos, flags);
 
    wxString text;
 
    if ( flags & wxODCB_PAINTING_CONTROL )
    {
        // Don't draw anything in the editable selection field.
        if ( !HasFlag(wxCB_READONLY) )
            return true;
 
        pos = GetSelection();
        // Skip drawing if there is nothing selected.
        if ( pos < 0 )
            return true;
 
        text = GetValue();
    }
    else
    {
        text = GetString(pos);
    }
 
    wxBitmapComboBoxBase::DrawItem(dc, rect, pos, text, flags);
 
    // If the item has the focus, draw focus rectangle.
    // Commented out since regular combo box doesn't
    // seem to do it either.
    //if ( lpDrawItem->itemState & ODS_FOCUS )
    //    DrawFocusRect(lpDrawItem->hDC, &lpDrawItem->rcItem);
 
    return true;
}
 
bool wxBitmapComboBox::MSWOnMeasure(WXMEASUREITEMSTRUCT *item)
{
    LPMEASUREITEMSTRUCT lpMeasureItem = (LPMEASUREITEMSTRUCT) item;
    int pos = lpMeasureItem->itemID;
 
    // Measure edit field height if item list is not empty,
    // otherwise leave default system value.
    if ( m_usedImgSize.y >= 0 || pos >= 0 )
    {
        lpMeasureItem->itemHeight = wxBitmapComboBoxBase::MeasureItem(pos);
    }
 
    return true;
}
 
#endif // wxUSE_BITMAPCOMBOBOX

V821 Decreased performance. The 'value' variable can be constructed in a lower level scope.