]> git.saurik.com Git - wxWidgets.git/blobdiff - src/msw/headerctrl.cpp
Fixed post-expand/collapse rendering
[wxWidgets.git] / src / msw / headerctrl.cpp
index 3acf89cd38141d6c1144c13b09dcfd53d3f63f92..4983a1dcf45ade8f2c68fb72f7bebdc4ba2cbbf3 100644 (file)
     #pragma hdrstop
 #endif
 
+#if wxUSE_HEADERCTRL
+
 #ifndef WX_PRECOMP
+    #include "wx/app.h"
     #include "wx/log.h"
 #endif // WX_PRECOMP
 
 #include "wx/headerctrl.h"
+
+#ifndef wxHAS_GENERIC_HEADERCTRL
+
 #include "wx/imaglist.h"
 
 #include "wx/msw/wrapcctl.h"
 #include "wx/msw/private.h"
 
+#ifndef HDM_SETBITMAPMARGIN
+    #define HDM_SETBITMAPMARGIN 0x1234
+#endif
+
+#ifndef Header_SetBitmapMargin
+    #define Header_SetBitmapMargin(hwnd, margin) \
+            ::SendMessage((hwnd), HDM_SETBITMAPMARGIN, (WPARAM)(margin), 0)
+#endif
+
+// from src/msw/listctrl.cpp
+extern int WXDLLIMPEXP_CORE wxMSWGetColumnClicked(NMHDR *nmhdr, POINT *ptClick);
+
 // ============================================================================
 // wxHeaderCtrl implementation
 // ============================================================================
@@ -43,7 +61,9 @@
 
 void wxHeaderCtrl::Init()
 {
+    m_numColumns = 0;
     m_imageList = NULL;
+    m_scrollOffset = 0;
 }
 
 bool wxHeaderCtrl::Create(wxWindow *parent,
@@ -60,9 +80,19 @@ bool wxHeaderCtrl::Create(wxWindow *parent,
     if ( !CreateControl(parent, id, pos, size, style, wxDefaultValidator, name) )
         return false;
 
-    if ( !MSWCreateControl(WC_HEADER, _T(""), pos, size) )
+    if ( !MSWCreateControl(WC_HEADER, wxT(""), pos, size) )
         return false;
 
+    // special hack for margins when using comctl32.dll v6 or later: the
+    // default margin is too big and results in label truncation when the
+    // column width is just about right to show it together with the sort
+    // indicator, so reduce it to a smaller value (in principle we could even
+    // use 0 here but this starts to look ugly)
+    if ( wxApp::GetComCtl32Version() >= 600 )
+    {
+        Header_SetBitmapMargin(GetHwnd(), ::GetSystemMetrics(SM_CXEDGE));
+    }
+
     return true;
 }
 
@@ -70,13 +100,15 @@ WXDWORD wxHeaderCtrl::MSWGetStyle(long style, WXDWORD *exstyle) const
 {
     WXDWORD msStyle = wxControl::MSWGetStyle(style, exstyle);
 
-    if ( style & wxHD_DRAGDROP )
+    if ( style & wxHD_ALLOW_REORDER )
         msStyle |= HDS_DRAGDROP;
 
     // the control looks nicer with these styles and there doesn't seem to be
     // any reason to not use them so we always do (as for HDS_HORZ it is 0
     // anyhow but include it for clarity)
-    msStyle |= HDS_HORZ | HDS_BUTTONS | HDS_FLAT | HDS_FULLDRAG | HDS_HOTTRACK;
+    // NOTE: don't use however HDS_FLAT because it makes the control look
+    //       non-native when running WinXP in classic mode
+    msStyle |= HDS_HORZ | HDS_BUTTONS | HDS_FULLDRAG | HDS_HOTTRACK;
 
     return msStyle;
 }
@@ -86,6 +118,33 @@ wxHeaderCtrl::~wxHeaderCtrl()
     delete m_imageList;
 }
 
+// ----------------------------------------------------------------------------
+// wxHeaderCtrl scrolling
+// ----------------------------------------------------------------------------
+
+void wxHeaderCtrl::DoSetSize(int x, int y,
+                             int w, int h,
+                             int sizeFlags)
+{
+    wxHeaderCtrlBase::DoSetSize(x + m_scrollOffset, y, w - m_scrollOffset, h,
+                                sizeFlags);
+}
+
+void wxHeaderCtrl::DoScrollHorz(int dx)
+{
+    // as the native control doesn't support offsetting its contents, we use a
+    // hack here to make it appear correctly when the parent is scrolled:
+    // instead of scrolling or repainting we simply move the control window
+    // itself: to be precise, offset it by the scroll increment to the left and
+    // increment its width to still extend to the right boundary to compensate
+    // for it (notice that dx is negative when scrolling to the right)
+    m_scrollOffset += dx;
+
+    wxHeaderCtrlBase::DoSetSize(GetPosition().x + dx, -1,
+                                GetSize().x - dx, -1,
+                                wxSIZE_USE_EXISTING);
+}
+
 // ----------------------------------------------------------------------------
 // wxHeaderCtrl geometry calculation
 // ----------------------------------------------------------------------------
@@ -97,7 +156,7 @@ wxSize wxHeaderCtrl::DoGetBestSize() const
     HDLAYOUT layout = { &rc, &wpos };
     if ( !Header_Layout(GetHwnd(), &layout) )
     {
-        wxLogLastError(_T("Header_Layout"));
+        wxLogLastError(wxT("Header_Layout"));
         return wxControl::DoGetBestSize();
     }
 
@@ -110,78 +169,526 @@ wxSize wxHeaderCtrl::DoGetBestSize() const
 
 unsigned int wxHeaderCtrl::DoGetCount() const
 {
-    return Header_GetItemCount(GetHwnd());
+    // we can't use Header_GetItemCount() here because it doesn't take the
+    // hidden columns into account and we can't find the hidden columns after
+    // the last shown one in MSWFromNativeIdx() without knowing where to stop
+    // so we have to store the columns count internally
+    return m_numColumns;
+}
+
+int wxHeaderCtrl::GetShownColumnsCount() const
+{
+    const int numItems = Header_GetItemCount(GetHwnd());
+
+    wxASSERT_MSG( numItems >= 0 && (unsigned)numItems <= m_numColumns,
+                  "unexpected number of items in the native control" );
+
+    return numItems;
+}
+
+void wxHeaderCtrl::DoSetCount(unsigned int count)
+{
+    unsigned n;
+
+    // first delete all old columns
+    const unsigned countOld = GetShownColumnsCount();
+    for ( n = 0; n < countOld; n++ )
+    {
+        if ( !Header_DeleteItem(GetHwnd(), 0) )
+        {
+            wxLogLastError(wxT("Header_DeleteItem"));
+        }
+    }
+
+    // update the column indices order array before changing m_numColumns
+    DoResizeColumnIndices(m_colIndices, count);
+
+    // and add the new ones
+    m_numColumns = count;
+    m_isHidden.resize(m_numColumns);
+    for ( n = 0; n < count; n++ )
+    {
+        const wxHeaderColumn& col = GetColumn(n);
+        if ( col.IsShown() )
+        {
+            m_isHidden[n] = false;
+
+            DoInsertItem(col, n);
+        }
+        else // hidden initially
+        {
+            m_isHidden[n] = true;
+        }
+    }
+}
+
+void wxHeaderCtrl::DoUpdate(unsigned int idx)
+{
+    // the native control does provide Header_SetItem() but it's inconvenient
+    // to use it because it sends HDN_ITEMCHANGING messages and we'd have to
+    // arrange not to block setting the width from there and the logic would be
+    // more complicated as we'd have to reset the old values as well as setting
+    // the new ones -- so instead just recreate the column
+
+    const wxHeaderColumn& col = GetColumn(idx);
+    if ( col.IsHidden() )
+    {
+        // column is hidden now
+        if ( !m_isHidden[idx] )
+        {
+            // but it wasn't hidden before, so remove it
+            Header_DeleteItem(GetHwnd(), MSWToNativeIdx(idx));
+
+            m_isHidden[idx] = true;
+        }
+        //else: nothing to do, updating hidden column doesn't have any effect
+    }
+    else // column is shown now
+    {
+        if ( m_isHidden[idx] )
+        {
+            m_isHidden[idx] = false;
+        }
+        else // and it was shown before as well
+        {
+            // we need to remove the old column
+            Header_DeleteItem(GetHwnd(), MSWToNativeIdx(idx));
+        }
+
+        DoInsertItem(col, idx);
+    }
 }
 
-void wxHeaderCtrl::DoInsert(const wxHeaderColumn& col, unsigned int idx)
+void wxHeaderCtrl::DoInsertItem(const wxHeaderColumn& col, unsigned int idx)
 {
-    // copy the HDITEM because we may modify it below
-    HDITEM hdi = col.GetHDI();
+    wxASSERT_MSG( !col.IsHidden(), "should only be called for shown columns" );
+
+    wxHDITEM hdi;
+
+    // notice that we need to store the string we use the pointer to until we
+    // pass it to the control
+    hdi.mask |= HDI_TEXT;
+    wxWxCharBuffer buf = col.GetTitle().wx_str();
+    hdi.pszText = buf.data();
+    hdi.cchTextMax = wxStrlen(buf);
 
     const wxBitmap bmp = col.GetBitmap();
     if ( bmp.IsOk() )
     {
-        const int bmpWidth = bmp.GetWidth(),
-                  bmpHeight = bmp.GetHeight();
+        hdi.mask |= HDI_IMAGE;
 
-        if ( !m_imageList )
+        if ( bmp.IsOk() )
         {
-            m_imageList = new wxImageList(bmpWidth, bmpHeight);
-            Header_SetImageList(GetHwnd(), GetHimagelistOf(m_imageList));
+            const int bmpWidth = bmp.GetWidth(),
+                      bmpHeight = bmp.GetHeight();
+
+            if ( !m_imageList )
+            {
+                m_imageList = new wxImageList(bmpWidth, bmpHeight);
+                (void) // suppress mingw32 warning about unused computed value
+                Header_SetImageList(GetHwnd(), GetHimagelistOf(m_imageList));
+            }
+            else // already have an image list
+            {
+                // check that all bitmaps we use have the same size
+                int imageWidth,
+                    imageHeight;
+                m_imageList->GetSize(0, imageWidth, imageHeight);
+
+                wxASSERT_MSG( imageWidth == bmpWidth && imageHeight == bmpHeight,
+                              "all column bitmaps must have the same size" );
+            }
+
+            m_imageList->Add(bmp);
+            hdi.iImage = m_imageList->GetImageCount() - 1;
         }
-        else // already have an image list
+        else // no bitmap but we still need to update the item
         {
-            // check that all bitmaps we use have the same size
-            int imageWidth,
-                imageHeight;
-            m_imageList->GetSize(0, imageWidth, imageHeight);
+            hdi.iImage = I_IMAGENONE;
+        }
+    }
+
+    if ( col.GetAlignment() != wxALIGN_NOT )
+    {
+        hdi.mask |= HDI_FORMAT | HDF_LEFT;
+        switch ( col.GetAlignment() )
+        {
+            case wxALIGN_LEFT:
+                hdi.fmt |= HDF_LEFT;
+                break;
 
-            wxASSERT_MSG( imageWidth == bmpWidth && imageHeight == bmpHeight,
-                          "all column bitmaps must have the same size" );
+            case wxALIGN_CENTER:
+            case wxALIGN_CENTER_HORIZONTAL:
+                hdi.fmt |= HDF_CENTER;
+                break;
+
+            case wxALIGN_RIGHT:
+                hdi.fmt |= HDF_RIGHT;
+                break;
+
+            default:
+                wxFAIL_MSG( "invalid column header alignment" );
         }
+    }
 
-        m_imageList->Add(bmp);
-        hdi.mask |= HDI_IMAGE;
-        hdi.iImage = m_imageList->GetImageCount() - 1;
+    if ( col.IsSortKey() )
+    {
+        hdi.mask |= HDI_FORMAT;
+        hdi.fmt |= col.IsSortOrderAscending() ? HDF_SORTUP : HDF_SORTDOWN;
     }
 
-    if ( Header_InsertItem(GetHwnd(), idx, &hdi) == -1 )
+    if ( col.GetWidth() != wxCOL_WIDTH_DEFAULT )
+    {
+        hdi.mask |= HDI_WIDTH;
+        hdi.cxy = col.GetWidth();
+    }
+
+    hdi.mask |= HDI_ORDER;
+    hdi.iOrder = MSWToNativeOrder(m_colIndices.Index(idx));
+
+    if ( ::SendMessage(GetHwnd(), HDM_INSERTITEM,
+                       MSWToNativeIdx(idx), (LPARAM)&hdi) == -1 )
     {
-        wxLogLastError(_T("Header_InsertItem"));
+        wxLogLastError(wxT("Header_InsertItem()"));
     }
 }
 
-void wxHeaderCtrl::DoDelete(unsigned int idx)
+void wxHeaderCtrl::DoSetColumnsOrder(const wxArrayInt& order)
 {
-    if ( !Header_DeleteItem(GetHwnd(), idx) )
+    wxArrayInt orderShown;
+    orderShown.reserve(m_numColumns);
+
+    for ( unsigned n = 0; n < m_numColumns; n++ )
+    {
+        const int idx = order[n];
+        if ( GetColumn(idx).IsShown() )
+            orderShown.push_back(MSWToNativeIdx(idx));
+    }
+
+    if ( !Header_SetOrderArray(GetHwnd(), orderShown.size(), &orderShown[0]) )
     {
-        wxLogLastError(_T("Header_DeleteItem"));
+        wxLogLastError(wxT("Header_GetOrderArray"));
     }
+
+    m_colIndices = order;
+}
+
+wxArrayInt wxHeaderCtrl::DoGetColumnsOrder() const
+{
+    // we don't use Header_GetOrderArray() here because it doesn't return
+    // information about the hidden columns, instead we just save the columns
+    // order array in DoSetColumnsOrder() and update it when they're reordered
+    return m_colIndices;
 }
 
 // ----------------------------------------------------------------------------
-// wxHeaderCtrl columns attributes
+// wxHeaderCtrl indexes and positions translation
 // ----------------------------------------------------------------------------
 
-void wxHeaderCtrl::DoShowSortIndicator(unsigned int idx, int sortOrder)
+int wxHeaderCtrl::MSWToNativeIdx(int idx)
 {
-    wxHDITEM hdi;
-    hdi.mask = HDI_FORMAT;
+    // don't check for GetColumn(idx).IsShown() as it could have just became
+    // false and we may be called from DoUpdate() to delete the old column
+    wxASSERT_MSG( !m_isHidden[idx],
+                  "column must be visible to have an "
+                  "index in the native control" );
+
+    int item = idx;
+    for ( int i = 0; i < idx; i++ )
+    {
+        if ( GetColumn(i).IsHidden() )
+            item--; // one less column the native control knows about
+    }
+
+    wxASSERT_MSG( item >= 0 && item <= GetShownColumnsCount(), "logic error" );
+
+    return item;
+}
+
+int wxHeaderCtrl::MSWFromNativeIdx(int item)
+{
+    wxASSERT_MSG( item >= 0 && item < GetShownColumnsCount(),
+                  "column index out of range" );
+
+    // reverse the above function
+
+    unsigned idx = item;
+    for ( unsigned n = 0; n < m_numColumns; n++ )
+    {
+        if ( n > idx )
+            break;
+
+        if ( GetColumn(n).IsHidden() )
+            idx++;
+    }
+
+    wxASSERT_MSG( MSWToNativeIdx(idx) == item, "logic error" );
+
+    return idx;
+}
+
+int wxHeaderCtrl::MSWToNativeOrder(int pos)
+{
+    wxASSERT_MSG( pos >= 0 && static_cast<unsigned>(pos) < m_numColumns,
+                  "column position out of range" );
+
+    int order = pos;
+    for ( int n = 0; n < pos; n++ )
+    {
+        if ( GetColumn(m_colIndices[n]).IsHidden() )
+            order--;
+    }
+
+    wxASSERT_MSG( order >= 0 && order <= GetShownColumnsCount(), "logic error" );
 
-    if ( !Header_GetItem(GetHwnd(), idx, &hdi) )
+    return order;
+}
+
+int wxHeaderCtrl::MSWFromNativeOrder(int order)
+{
+    wxASSERT_MSG( order >= 0 && order < GetShownColumnsCount(),
+                  "native column position out of range" );
+
+    unsigned pos = order;
+    for ( unsigned n = 0; n < m_numColumns; n++ )
     {
-        wxLogLastError(_T("Header_GetItem"));
-        return;
+        if ( n > pos )
+            break;
+
+        if ( GetColumn(m_colIndices[n]).IsHidden() )
+            pos++;
     }
 
-    if ( sortOrder == -1 )
-        hdi.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
-    else
-        hdi.fmt |= sortOrder ? HDF_SORTUP : HDF_SORTDOWN;
+    wxASSERT_MSG( MSWToNativeOrder(pos) == order, "logic error" );
+
+    return pos;
+}
+
+// ----------------------------------------------------------------------------
+// wxHeaderCtrl events
+// ----------------------------------------------------------------------------
 
-    if ( !Header_SetItem(GetHwnd(), idx, &hdi) )
+wxEventType wxHeaderCtrl::GetClickEventType(bool dblclk, int button)
+{
+    wxEventType evtType;
+    switch ( button )
     {
-        wxLogLastError(_T("Header_SetItem"));
+        case 0:
+            evtType = dblclk ? wxEVT_COMMAND_HEADER_DCLICK
+                             : wxEVT_COMMAND_HEADER_CLICK;
+            break;
+
+        case 1:
+            evtType = dblclk ? wxEVT_COMMAND_HEADER_RIGHT_DCLICK
+                             : wxEVT_COMMAND_HEADER_RIGHT_CLICK;
+            break;
+
+        case 2:
+            evtType = dblclk ? wxEVT_COMMAND_HEADER_MIDDLE_DCLICK
+                             : wxEVT_COMMAND_HEADER_MIDDLE_CLICK;
+            break;
+
+        default:
+            wxFAIL_MSG( wxS("unexpected event type") );
+            evtType = wxEVT_NULL;
     }
+
+    return evtType;
 }
 
+bool wxHeaderCtrl::MSWOnNotify(int idCtrl, WXLPARAM lParam, WXLPARAM *result)
+{
+    NMHEADER * const nmhdr = (NMHEADER *)lParam;
+
+    wxEventType evtType = wxEVT_NULL;
+    int width = 0;
+    int order = -1;
+    bool veto = false;
+    const UINT code = nmhdr->hdr.code;
+
+    // we don't have the index for all events, e.g. not for NM_RELEASEDCAPTURE
+    // so only access for header control events (and yes, the direction of
+    // comparisons with FIRST/LAST is correct even if it seems inverted)
+    int idx = code <= HDN_FIRST && code > HDN_LAST ? nmhdr->iItem : -1;
+    if ( idx != -1 )
+    {
+        // we also get bogus HDN_BEGINDRAG with -1 index so don't call
+        // MSWFromNativeIdx() unconditionally for nmhdr->iItem
+        idx = MSWFromNativeIdx(idx);
+    }
+
+    switch ( code )
+    {
+        // click events
+        // ------------
+
+        case HDN_ITEMCLICK:
+        case HDN_ITEMDBLCLICK:
+            evtType = GetClickEventType(code == HDN_ITEMDBLCLICK, nmhdr->iButton);
+            break;
+
+            // although we should get the notifications about the right clicks
+            // via HDN_ITEM[DBL]CLICK too according to MSDN this simply doesn't
+            // happen in practice on any Windows system up to 2003
+        case NM_RCLICK:
+        case NM_RDBLCLK:
+            {
+                POINT pt;
+                idx = wxMSWGetColumnClicked(&nmhdr->hdr, &pt);
+                if ( idx != wxNOT_FOUND )
+                {
+                    idx = MSWFromNativeIdx(idx);
+
+                    // due to a bug in mingw32 headers NM_RDBLCLK is signed
+                    // there so we need a cast to avoid warnings about signed/
+                    // unsigned comparison
+                    evtType = GetClickEventType(
+                                code == static_cast<UINT>(NM_RDBLCLK), 1);
+                }
+                //else: ignore clicks outside any column
+            }
+            break;
+
+        case HDN_DIVIDERDBLCLICK:
+            evtType = wxEVT_COMMAND_HEADER_SEPARATOR_DCLICK;
+            break;
+
+
+        // column resizing events
+        // ----------------------
+
+        // see comments in wxListCtrl::MSWOnNotify() for why we catch both
+        // ASCII and Unicode versions of this message
+        case HDN_BEGINTRACKA:
+        case HDN_BEGINTRACKW:
+            // non-resizeable columns can't be resized no matter what, don't
+            // even generate any events for them
+            if ( !GetColumn(idx).IsResizeable() )
+            {
+                veto = true;
+                break;
+            }
+
+            evtType = wxEVT_COMMAND_HEADER_BEGIN_RESIZE;
+            // fall through
+
+        case HDN_ENDTRACKA:
+        case HDN_ENDTRACKW:
+            width = nmhdr->pitem->cxy;
+
+            if ( evtType == wxEVT_NULL )
+            {
+                evtType = wxEVT_COMMAND_HEADER_END_RESIZE;
+
+                // don't generate events with invalid width
+                const int minWidth = GetColumn(idx).GetMinWidth();
+                if ( width < minWidth )
+                    width = minWidth;
+            }
+            break;
+
+        case HDN_ITEMCHANGING:
+            if ( nmhdr->pitem && (nmhdr->pitem->mask & HDI_WIDTH) )
+            {
+                // prevent the column from being shrunk beneath its min width
+                width = nmhdr->pitem->cxy;
+                if ( width < GetColumn(idx).GetMinWidth() )
+                {
+                    // don't generate any events and prevent the change from
+                    // happening
+                    veto = true;
+                }
+                else // width is acceptable
+                {
+                    // generate the resizing event from here as we don't seem
+                    // to be getting HDN_TRACK events at all, at least with
+                    // comctl32.dll v6
+                    evtType = wxEVT_COMMAND_HEADER_RESIZING;
+                }
+            }
+            break;
+
+
+        // column reordering events
+        // ------------------------
+
+        case HDN_BEGINDRAG:
+            // Windows sometimes sends us events with invalid column indices
+            if ( nmhdr->iItem == -1 )
+                break;
+
+            // column must have the appropriate flag to be draggable
+            if ( !GetColumn(idx).IsReorderable() )
+            {
+                veto = true;
+                break;
+            }
+
+            evtType = wxEVT_COMMAND_HEADER_BEGIN_REORDER;
+            break;
+
+        case HDN_ENDDRAG:
+            wxASSERT_MSG( nmhdr->pitem->mask & HDI_ORDER, "should have order" );
+            order = nmhdr->pitem->iOrder;
+
+            // we also get messages with invalid order when column reordering
+            // is cancelled (e.g. by pressing Esc)
+            if ( order == -1 )
+                break;
+
+            order = MSWFromNativeOrder(order);
+
+            evtType = wxEVT_COMMAND_HEADER_END_REORDER;
+            break;
+
+        case NM_RELEASEDCAPTURE:
+            evtType = wxEVT_COMMAND_HEADER_DRAGGING_CANCELLED;
+            break;
+    }
+
+
+    // do generate the corresponding wx event
+    if ( evtType != wxEVT_NULL )
+    {
+        wxHeaderCtrlEvent event(evtType, GetId());
+        event.SetEventObject(this);
+        event.SetColumn(idx);
+        event.SetWidth(width);
+        if ( order != -1 )
+            event.SetNewOrder(order);
+
+        if ( GetEventHandler()->ProcessEvent(event) )
+        {
+            if ( event.IsAllowed() )
+                return true;    // skip default message handling below
+
+            // we need to veto the default handling of this message, don't
+            // return to execute the code in the "if veto" branch below
+            veto = true;
+        }
+        else // not processed
+        {
+            // special post-processing for HDN_ENDDRAG: we need to update the
+            // internal column indices array if this is allowed to go ahead as
+            // the native control is going to reorder its columns now
+            if ( evtType == wxEVT_COMMAND_HEADER_END_REORDER )
+                MoveColumnInOrderArray(m_colIndices, idx, order);
+        }
+    }
+
+    if ( veto )
+    {
+        // all of HDN_BEGIN{DRAG,TRACK}, HDN_TRACK and HDN_ITEMCHANGING
+        // interpret TRUE return value as meaning to stop the control
+        // default handling of the message
+        *result = TRUE;
+
+        return true;
+    }
+
+    return wxHeaderCtrlBase::MSWOnNotify(idCtrl, lParam, result);
+}
+
+#endif // wxHAS_GENERIC_HEADERCTRL
+
+#endif // wxUSE_HEADERCTRL