/*++

© 1998 Seagate Software, Inc.  All rights reserved

Module Name:

    IeList.cpp

Abstract:

    CIeList is a subclassed (owner-draw) list control that groups items into
    a 3D panel that have the same information in the indicated 
    sortColumn.

    The panels are created from tiles.  Each tile corresponds to one subitem
    in the list, and has the appropriate 3D edges so that the tiles together
    make up a panel.

    NOTE: The control must be initialized with the number of columns and the
    sort column.  The parent dialog must implement OnMeasureItem and call
    GetItemHeight to set the row height for the control.

Author:

    Art Bragg [artb]   01-DEC-1997

Revision History:

--*/

#include "stdafx.h"
#include "IeList.h"

// Position of a tile in it's panel
#define POS_LEFT        100
#define POS_RIGHT       101
#define POS_TOP         102
#define POS_BOTTOM      103
#define POS_MIDDLE      104
#define POS_SINGLE      105


/////////////////////////////////////////////////////////////////////////////
// CIeList

BEGIN_MESSAGE_MAP(CIeList, CListCtrl)
    //{{AFX_MSG_MAP(CIeList)
    ON_NOTIFY_REFLECT(NM_CLICK, OnClick)
    ON_WM_SETFOCUS()
    ON_WM_KILLFOCUS()
    //}}AFX_MSG_MAP
    ON_WM_SYSCOLORCHANGE()

END_MESSAGE_MAP()

CIeList::CIeList()
/*++

Routine Description:

    Sets default dimensions for the control.

Arguments:

    none.

Return Value:

    none.

--*/
{
    //
    // Initializations
    //
    m_ColCount = 0;
    m_SortCol = 0;
    m_pVertPos = NULL;
    //
    // Drawing dimensions
    //
    // If these are altered, the visual aspects of the control
    // should be checked (especially the focus rectangle), 
    // as some minor adjustments may need to be made.
    //
    m_VertRaisedSpace           = 1;
    m_BorderThickness           = 2;
    m_VerticalTextOffsetTop     = 1;

    // The text height will be set later (based on the font size)
    m_Textheight                = 0;
    m_VerticalTextOffsetBottom  = 1;

    // Total height will be set later
    m_TotalHeight               = 0;
    m_HorzRaisedSpace           = 1;
    m_HorzTextOffset            = 3;

}

CIeList::~CIeList()
/*++

Routine Description:

    Cleanup.

Arguments:

    none.

Return Value:

    none.

--*/
{
    // Cleanup the array of vertical positions
    if( m_pVertPos ) free ( m_pVertPos );
}

/////////////////////////////////////////////////////////////////////////////
// CIeList message handlers

void CIeList::Initialize( 
    IN int colCount, 
    IN int sortCol 
    )
/*++

Routine Description:

    Sets the number of columns (not easily available from MFC) and
    the sort column.

Arguments:

    colCount        - number of columns to display
    sortCol         - column to sort on

Return Value:

    none.

--*/
{

    m_ColCount = colCount;
    m_SortCol = sortCol;
}

void CIeList::DrawItem(
    IN LPDRAWITEMSTRUCT lpDrawItemStruct
    ) 
/*++

Routine Description:

    This is the callback for an owner draw control.
    Draws the appropriate text and/or 3D lines depending on the
    item number and clipping rectangle supplied by MFC in lpDrawItemStruct 

Arguments:

    lpDrawItemStruct - MFC structure that tells us what and where to draw

Return Value:

    none.

--*/
{
    CDC dc;
    int saveDc;

    int colWidth = 0;                   // Width of current column
    int horzPos = POS_MIDDLE;           // Horz position in the panel
    int vertPos = POS_SINGLE;           // Vert position in the panel
    BOOL bSelected = FALSE;             // Is this item selected
    CRect rcAllLabels;                  // Used to find left position of focus rectangle
    CRect itemRect;                     // Rectangle supplied in lpDrawItemStruct
    CRect textRect;                     // Rectangle for text
    CRect boxRect;                      // Rectangle for 3D box (the panel)
    CRect clipRect;                     // Current clipping rectangle
    LPTSTR pszText;                    // Text to display
    COLORREF clrTextSave = 0;           // Save the current color
    COLORREF clrBkSave = 0;             // Save the background color
    int leftStart = 0;                  // Left edge of where we're currently drawing
    BOOL bFocus = (GetFocus() == this); // Do we have focus?

    //
    // Get the current scroll position
    //
    int nHScrollPos = GetScrollPos( SB_HORZ );

    //
    // Get the item ID from the list for the item we're drawing
    //
    int itemID = lpDrawItemStruct->itemID;

    //
    // Get item data for the item we're drawing
    //
    LV_ITEM lvi;
    lvi.mask = LVIF_IMAGE | LVIF_STATE;
    lvi.iItem = itemID;
    lvi.iSubItem = 0;
    lvi.stateMask = 0xFFFF;     // get all state flags
    GetItem(&lvi);

    //
    // Determine focus and selected states
    //
    bSelected = (bFocus || (GetStyle() & LVS_SHOWSELALWAYS)) && lvi.state & LVIS_SELECTED;

    //
    // Get the rectangle to draw in
    //
    itemRect = lpDrawItemStruct->rcItem;

    dc.Attach( lpDrawItemStruct->hDC );
    saveDc = dc.SaveDC();
    //
    // Get the clipping rectangle - we use it's vertical edges
    // to optimize what we draw
    //
    dc.GetClipBox( &clipRect );
    boxRect = clipRect;

    //
    // For each column, paint it's text and the section of the 3D panel
    //
    for ( int col = 0; col < m_ColCount; col++ ) {

        colWidth = GetColumnWidth( col );
        //
        // Only paint this column if it's in the clipping rectangle
        //
        if( ( ( leftStart + colWidth ) > clipRect.left ) || ( leftStart < clipRect.right ) ) {

            //
            // Determine the horizontal position based on the column
            //
            horzPos = POS_MIDDLE;
            if( col == 0 )                  horzPos = POS_LEFT;
            if( col == m_ColCount - 1 )     horzPos = POS_RIGHT;

            //
            // Calculate the rectangle for this tile
            //
            boxRect.top = itemRect.top;
            boxRect.bottom = itemRect.bottom;
            boxRect.left = itemRect.left + leftStart;
            boxRect.right = itemRect.left + leftStart + colWidth;

            //
            // Get the vertical position from the array.  It was saved there
            // during SortItem for performance reasons.
            //
            if( m_pVertPos ) { 

                vertPos = m_pVertPos[ itemID ];

            }

            //
            // Draw the tile for this item.
            //
            Draw3dRectx ( &dc, boxRect, horzPos, vertPos, bSelected );

            //
            // If this item is selected, change the text colors
            //
            if( bSelected ) {

                clrTextSave = dc.SetTextColor( m_clrHighlightText );
                clrBkSave = dc.SetBkColor( m_clrHighlight );

            }

            //
            // Calculate the text rectangle
            //
            textRect.top =      itemRect.top + m_VertRaisedSpace + m_BorderThickness + m_VerticalTextOffsetTop;
            textRect.bottom =   itemRect.bottom;    // Text is top justified, no need to adjust bottom
            textRect.left =     leftStart - nHScrollPos + m_HorzRaisedSpace + m_BorderThickness + m_HorzTextOffset;
            textRect.right =    itemRect.right;

            //
            // Get the text and put in the "..." if we need them
            //
            CString pszLongText = GetItemText( itemID, col );
            pszText = NULL;
            MakeShortString(&dc, (LPCTSTR) pszLongText,
                textRect.right - textRect.left, 4, &pszText);
            BOOL bFree = TRUE;
            if (pszText == NULL) {
                // Failure of some kind...
                pszText = (LPTSTR)(LPCTSTR)pszLongText;
                bFree = FALSE;
            }

            //
            // Now draw the text using the correct color
            //
            COLORREF saveTextColor;
            if( bSelected ) {

                saveTextColor = dc.SetTextColor( m_clrHighlightText );

            } else {

                saveTextColor = dc.SetTextColor( m_clrText );

            }
            int textheight = dc.DrawText( pszText, textRect, DT_NOCLIP | DT_LEFT | DT_TOP | DT_SINGLELINE  );
            dc.SetTextColor( saveTextColor );

            if (pszText && bFree) {
                free(pszText);
            }

        }

        //
        // Move to the next column
        //
        leftStart += colWidth;
    }
    //
    // draw focus rectangle if item has focus.  Use LVIR_BOUNDS rectangle
    // to bound it.
    //
    GetItemRect(itemID, rcAllLabels, LVIR_BOUNDS);
    if( lvi.state & LVIS_FOCUSED && bFocus ) {

        CRect focusRect;
        focusRect.left = rcAllLabels.left + m_HorzRaisedSpace + m_BorderThickness;
        focusRect.right = min( rcAllLabels.right, (itemRect.right - m_HorzRaisedSpace * 2 - 3) );
        focusRect.top = boxRect.top + m_VertRaisedSpace + m_BorderThickness;
        focusRect.bottom = boxRect.top + m_TotalHeight - m_BorderThickness + 1;

        dc.DrawFocusRect( focusRect );

    }

    // Restore colors
    if( bSelected ) {

        dc.SetTextColor( clrTextSave );
        dc.SetBkColor( clrBkSave );

    }

    dc.RestoreDC( saveDc );
    dc.Detach();
}


void CIeList::MakeShortString(
    IN CDC* pDC, 
    IN LPCTSTR lpszLong, 
    IN int nColumnLen, 
    IN int nDotOffset,
    OUT LPTSTR *ppszShort
    )
/*++

Routine Description:

    Determines it the supplied string fits in it's column.  If not truncates
    it and adds "...".  From MS sample code.

Arguments:

    pDC         - Device context
    lpszLong    - Original String
    nColumnLen  - Width of column
    nDotOffset  - Space before dots

Return Value:

    Shortened string

--*/
{
    static const _TCHAR szThreeDots[] = _T("...");

    int nStringLen = lstrlen(lpszLong);

    *ppszShort = (_TCHAR *)malloc((nStringLen + 1) * sizeof(_TCHAR) + sizeof(szThreeDots));
    if (*ppszShort == NULL)
        return;

    _TCHAR *szShort = *ppszShort;
    if(nStringLen == 0) {
        lstrcpy(szShort, _T(""));
    } else {
        lstrcpy(szShort, lpszLong);
    }

    if(nStringLen == 0 ||
        (pDC->GetTextExtent(lpszLong, nStringLen).cx + nDotOffset) <= nColumnLen)
    {
        // return long format
        return;
    }

    int nAddLen = pDC->GetTextExtent(szThreeDots,sizeof(szThreeDots)).cx;

    for(int i = nStringLen-1; i > 0; i--)
    {
        szShort[i] = 0;
        if((pDC->GetTextExtent(szShort, i).cx + nDotOffset + nAddLen)
            <= nColumnLen)
        {
            break;
        }
    }

    lstrcat(szShort, szThreeDots);
    return;
}

void CIeList::Draw3dRectx ( 
    IN CDC *pDc, 
    IN CRect &rect, 
    IN int horzPos, 
    IN int vertPos, 
    IN BOOL bSelected 
) 
/*++

Routine Description:

    Draws the appropriate portion (tile) of a panel for a given cell in the
    list.  The edges of the panel portion are determined by the horzPos
    and vertPos parameters.

Arguments:

    pDc         - Device context
    rect        - Rectangle to draw the panel portion in
    horzPos     - Where the portion is horizontally
    vertPos     - Where the portion is vertically
    bSelected   - Is the item selected

Return Value:

    none.

--*/

{

    CPen *pSavePen;
    int topOffset = 0;
    int rightOffset = 0;
    int leftOffset = 0;

    //
    // If a given edge of the tile is to be drawn, set an offset to that
    // edge.  If we don't draw a given edge, the offset is 0.
    //
    switch ( horzPos )
    {
    case POS_LEFT:
        leftOffset = m_HorzRaisedSpace;
        rightOffset = 0;
        break;
    case POS_MIDDLE:
        leftOffset = 0;
        rightOffset = 0;
        break;
    case POS_RIGHT:
        leftOffset = 0;
        rightOffset = m_HorzRaisedSpace + 3;
        break;
    }
    
    switch ( vertPos )
    {

    case POS_TOP:
        topOffset = m_VertRaisedSpace;
        break;
    case POS_MIDDLE:
        topOffset = 0;
        break;
    case POS_BOTTOM:
        topOffset = 0;
        break;
    case POS_SINGLE:
        topOffset = m_VertRaisedSpace;
        break;

    }
    //
    // Erase 
    //
    if( !bSelected ) pDc->FillSolidRect( rect, m_clrBkgnd );
    //
    // Highlight the selected area
    //
    if (bSelected)
    {
        CRect selectRect;
        if (leftOffset == 0)
            selectRect.left = rect.left;
        else
            selectRect.left = rect.left + leftOffset + m_BorderThickness;
        if (rightOffset == 0)
            selectRect.right = rect.right;
        else
            selectRect.right = rect.right - rightOffset - m_BorderThickness + 1;
        selectRect.top = rect.top + m_VertRaisedSpace + m_BorderThickness;
        selectRect.bottom = rect.top + m_TotalHeight - m_BorderThickness + 1;

        pDc->FillSolidRect( selectRect, m_clrHighlight );
    }

    // Select a pen to save the original pen
    pSavePen = pDc->SelectObject( &m_ShadowPen );

    // left edge
    if( horzPos == POS_LEFT ) {
        // Outside lighter line
        pDc->SelectObject( &m_ShadowPen );
        pDc->MoveTo( rect.left + leftOffset, rect.top + topOffset );
        pDc->LineTo( rect.left + leftOffset, rect.top + m_TotalHeight + 1);
        // Inside edge - darker line
        pDc->SelectObject( &m_DarkShadowPen );
        pDc->MoveTo( rect.left + leftOffset + 1, rect.top + topOffset);
        pDc->LineTo( rect.left + leftOffset + 1, rect.top + m_TotalHeight + 1);
    }
    // right edge
    if( horzPos == POS_RIGHT ) {
        // Outside line
        pDc->SelectObject( &m_HiLightPen );
        pDc->MoveTo( rect.right - rightOffset, rect.top + topOffset );
        pDc->LineTo( rect.right - rightOffset, rect.top + m_TotalHeight + 1 );
        // Inside line
        pDc->SelectObject( &m_LightPen );// note - this is usually the same color as btnface
        if( vertPos == POS_TOP )
            pDc->MoveTo( rect.right - rightOffset - 1, rect.top + topOffset + 1 );
        else
            pDc->MoveTo( rect.right - rightOffset - 1, rect.top + topOffset );
        pDc->LineTo( rect.right - rightOffset - 1, rect.top + m_TotalHeight + 2 );
    }
    // top edge
    if( ( vertPos == POS_TOP ) || ( vertPos == POS_SINGLE ) ) {
        // Outside lighter
        pDc->SelectObject( &m_ShadowPen );
        pDc->MoveTo( rect.left + leftOffset, rect.top + topOffset );
        pDc->LineTo( rect.right - rightOffset + 1, rect.top + topOffset );
        // Inside edge darker
        pDc->SelectObject( &m_DarkShadowPen );
        if( horzPos == POS_LEFT )
            pDc->MoveTo( rect.left + leftOffset + 1, rect.top + topOffset + 1 );
        else
            pDc->MoveTo( rect.left + leftOffset - 3, rect.top + topOffset + 1 );
        pDc->LineTo( rect.right - rightOffset, rect.top + topOffset + 1);
    }
    // bottom edge
    if( ( vertPos == POS_BOTTOM ) || ( vertPos == POS_SINGLE ) ) {
        // Outside line
        pDc->SelectObject( &m_HiLightPen );
        if( horzPos == POS_LEFT )
            pDc->MoveTo( rect.left + leftOffset + 1, rect.top + m_TotalHeight );
        else
            pDc->MoveTo( rect.left + leftOffset - 1, rect.top + m_TotalHeight );
        pDc->LineTo( rect.right - rightOffset, rect.top + m_TotalHeight );
        // Inside line
        pDc->SelectObject( &m_LightPen );
        if( horzPos == POS_LEFT )
            pDc->MoveTo( rect.left + leftOffset + 2, rect.top + m_TotalHeight - 1 );
        else
            pDc->MoveTo( rect.left + leftOffset - 2, rect.top + m_TotalHeight - 1 );
        pDc->LineTo( rect.right - rightOffset - 1, rect.top + m_TotalHeight - 1 );

    }
    pDc->SelectObject( pSavePen );

}

void CIeList::OnClick(
    NMHDR* /* pNMHDR */, LRESULT* pResult
) 
/*++

Routine Description:
    When the list is clicked, we invalidate the 
    rectangle for the currently selected item

Arguments:

    pResult     - ununsed

Return Value:

    none.

--*/
{
    CRect rect;

    // Get the selected item
    int curIndex = GetNextItem( -1, LVNI_SELECTED );
    if( curIndex != -1 ) {
        GetItemRect( curIndex, &rect, LVIR_BOUNDS );
        InvalidateRect( rect );
        UpdateWindow();
    }

    *pResult = 0;
}
/*++

Routine Description:
    Repaint the currently selected item if the style is LVS_SHOWSELALWAYS.

Arguments:

    none.

Return Value:

    none.

--*/

void CIeList::RepaintSelectedItems()
{
    CRect rcItem, rcLabel;
    //
    // invalidate focused item so it can repaint properly
    //
    int nItem = GetNextItem(-1, LVNI_FOCUSED);

    if(nItem != -1)
    {
        GetItemRect(nItem, rcItem, LVIR_BOUNDS);
        GetItemRect(nItem, rcLabel, LVIR_LABEL);
        rcItem.left = rcLabel.left;

        InvalidateRect(rcItem, FALSE);
    }
    //
    // if selected items should not be preserved, invalidate them
    //
    if(!(GetStyle() & LVS_SHOWSELALWAYS))
    {
        for(nItem = GetNextItem(-1, LVNI_SELECTED);
            nItem != -1; nItem = GetNextItem(nItem, LVNI_SELECTED))
        {
            GetItemRect(nItem, rcItem, LVIR_BOUNDS);
            GetItemRect(nItem, rcLabel, LVIR_LABEL);
            rcItem.left = rcLabel.left;

            InvalidateRect(rcItem, FALSE);
        }
    }

    // update changes 

    UpdateWindow();
}

int CIeList::GetItemHeight(
    IN LONG fontHeight
    ) 
/*++

Routine Description:
    Calculates the item height (the height of each drawing
    rectangle in the control) based on the supplied fontHeight.  This
    function is used by the parent to set the item height for the
    control.

Arguments:

    fontHeight - The height of the current font.

Return Value:

    Item height.

--*/

{


     int itemHeight = 
         m_VertRaisedSpace +
         m_BorderThickness +
         m_VerticalTextOffsetTop +
         fontHeight +
         2 +
         m_VerticalTextOffsetBottom +
         m_BorderThickness + 
         1;
     return itemHeight;
    
}

void CIeList::OnSetFocus(
    CWnd* pOldWnd
    ) 
/*++

Routine Description:
    Repaint the selected item.

Arguments:

    pOldWnd - Not used by this function

Return Value:

    none

--*/
{
    CListCtrl::OnSetFocus(pOldWnd);
    
    // repaint items that should change appearance
    RepaintSelectedItems();
        
}

void CIeList::OnKillFocus(
    CWnd* pNewWnd
) 
/*++

Routine Description:
    Repaint the selected item.

Arguments:

    pOldWnd - Not used by this function

Return Value:

    none

--*/
{
    CListCtrl::OnKillFocus(pNewWnd);
    
    // repaint items that should change appearance
    RepaintSelectedItems();
}

void CIeList::PreSubclassWindow() 
/*++

Routine Description:
    Calculate height parameters based on the font size.  Set
    colors for the control.

Arguments:

    none.

Return Value:

    none

--*/
{
    CFont *pFont;
    LOGFONT logFont; 

    pFont = GetFont( );
    pFont->GetLogFont( &logFont );

    LONG fontHeight = abs ( logFont.lfHeight );

    m_Textheight = fontHeight + 2;

    m_TotalHeight = 
         m_VertRaisedSpace +
         m_BorderThickness +
         m_VerticalTextOffsetTop +
         m_Textheight +
         m_VerticalTextOffsetBottom +
         m_BorderThickness; 

    SetColors();
    CListCtrl::PreSubclassWindow();
}

void CIeList::OnSysColorChange() 
/*++

Routine Description:
    Set the system colors and invalidate the control.

Arguments:

    none.

Return Value:

    none

--*/
{
    SetColors();
    Invalidate();
}

void CIeList::SetColors()
/*++

Routine Description:
    Store the system colors and create pens.

Arguments:

    none.

Return Value:

    none

--*/
{

    // Text colors
    m_clrText =             ::GetSysColor(COLOR_WINDOWTEXT);
    m_clrTextBk =           ::GetSysColor(COLOR_BTNFACE);
    m_clrBkgnd =            ::GetSysColor(COLOR_BTNFACE);
    m_clrHighlightText =    ::GetSysColor(COLOR_HIGHLIGHTTEXT);
    m_clrHighlight  =       ::GetSysColor(COLOR_HIGHLIGHT);

    // Line colors
    m_clr3DDkShadow =       ::GetSysColor( COLOR_3DDKSHADOW );
    m_clr3DShadow =         ::GetSysColor( COLOR_3DSHADOW );
    m_clr3DLight =          ::GetSysColor( COLOR_3DLIGHT );
    m_clr3DHiLight =        ::GetSysColor( COLOR_3DHIGHLIGHT );

    SetBkColor( m_clrBkgnd );
    SetTextColor( m_clrText );
    SetTextBkColor( m_clrTextBk );

    // Pens for 3D rectangles
    if( m_DarkShadowPen.GetSafeHandle() != NULL )
        m_DarkShadowPen.DeleteObject();
    m_DarkShadowPen.CreatePen ( PS_SOLID, 1, m_clr3DDkShadow );

    if( m_ShadowPen.GetSafeHandle() != NULL )
        m_ShadowPen.DeleteObject();
    m_ShadowPen.CreatePen ( PS_SOLID, 1, m_clr3DShadow );

    if( m_LightPen.GetSafeHandle() != NULL )
        m_LightPen.DeleteObject();
    m_LightPen.CreatePen ( PS_SOLID, 1, m_clr3DLight );

    if( m_HiLightPen.GetSafeHandle() != NULL )
        m_HiLightPen.DeleteObject();
    m_HiLightPen.CreatePen ( PS_SOLID, 1, m_clr3DHiLight );

}

BOOL CIeList::SortItems( 
    IN PFNLVCOMPARE pfnCompare, 
    IN DWORD dwData 
    )
/*++

Routine Description:
    Override for SortItems.  Checks the text of the sortColumn
    for each line in the control against it's neighbors (above and
    below) and assigns each line a position within it's panel.

Arguments:

    pfnCompare          - sort callback function
    dwData              - Unused

Return Value:

    TRUE, FALSE

--*/
{
    BOOL retVal = FALSE;
    BOOL bEqualAbove = FALSE;
    BOOL bEqualBelow = FALSE;
    CString thisText;
    CString aboveText;
    CString belowText;

    int numItems = GetItemCount();
    //
    // Call the base class to sort the items
    //
    if( CListCtrl::SortItems( pfnCompare, dwData ) ) {
        //
        // Get the vertical position (position within a panel) by comparing the text
        // of the sort column and stash it in the array of vertical positions
        //
        if( m_pVertPos ) {

            free( m_pVertPos );

        }
        m_pVertPos = (int *) malloc( numItems * sizeof( int ) );
        if( m_pVertPos ) {

            retVal = TRUE;

            for( int itemID = 0; itemID < numItems; itemID++ ) {
                //
                // Get the text of the item and it's neighbors
                //
                thisText = GetItemText( itemID, m_SortCol );
                aboveText = GetItemText( itemID - 1, m_SortCol );
                belowText = GetItemText( itemID + 1, m_SortCol );
                //
                // Set booleans for the relationship of this item to it's
                // neighbors
                //
                if( ( itemID == 0) || (  thisText.CompareNoCase( aboveText ) != 0 ) ){

                    bEqualAbove = FALSE;

                } else {

                    bEqualAbove = TRUE;

                }
                if( ( itemID == GetItemCount() - 1 ) || ( thisText.CompareNoCase( belowText ) != 0 ) ) {

                    bEqualBelow = FALSE;

                } else {

                    bEqualBelow = TRUE;

                }
                //
                // Determine the position in the panel
                //
                if      ( bEqualAbove && bEqualBelow )  m_pVertPos[ itemID ] = POS_MIDDLE;
                else if( bEqualAbove && !bEqualBelow ) m_pVertPos[ itemID ] = POS_BOTTOM;
                else if( !bEqualAbove && bEqualBelow ) m_pVertPos[ itemID ] = POS_TOP;
                else                                    m_pVertPos[ itemID ] = POS_SINGLE;
            }
        }
    }
    return( retVal );
}