[PATCH v18 0/1] MR10191: comctl32: Implement Tile view style
The Tile View in common controls offers a compact and pretty nice way to show some additional context within the list view, without resorting to the full reports mode. It's used a lot in explorer like programs where things like the disk size and file type are shown. But, it's been sitting unimplemented in wine for a long time. I've just spent a few days implementing this by: - Building a small test program in windows to get a feel for how it works - Hacking just enough to get something displaying - Writing a test suite to try and understand the ownership model, getting, and setting, and the various messages passed by windows. - Fixing up the implementation. - Adjusting my program to use I_COLUMNCALLBACK in windows and testing that. - Fixing my implementation to match the behaviour observed. Full disclosure, I originally started this implementation within the ReactOS build system as I had that compiling installed on my Windows box (my Mac is an M1 so doesn't play very nicely with Wine). I've since spun up a Linux VM and continued the work there. All bugs and misunderstandings are my own; I used clean room techniques (read MSDN, write a test program, write tests, compare results, iterate). Interesting things: - I initially wrote layout code for tiling mode as well, but a few rounds of playing with my test program and various layouts and settings (things like `LVS_ALIGNTOP`, `LVS_AUTOARRANGE` and `LVS_ALIGNLEFT`) for both tile mode and icon mode, convinced me that they use same layout logic (or close enough to). - I noticed some differences in Wine's Icon mode layout as compared to Windows 10. - I didn't fix these, one thing at a time. - I noticed and fixed a bug in SetView, where `UpdateItemSize` was called _after_ `Arrange`; that doesn't work. - The Text colours are really odd, if set to black the sub items show up gray, but any other colour and they're the same as the main. - Layout is tricky, in windows it looks like the first item (file name for example) can flow to 2 lines, and subitems are restricted to 1 - But this is really subtle, using GetItemMetrics kind of assumes there's no 'interference' from other items. - If we're on subitem 2, I don't want to have to fetch 0 and 1 to render their sizes before calculating my own, it seems like an `O(N^2)` situation off the bat. - I used a sidechannel from DrawItems, I'm open to other ideas here. - I haven't tested message passing, many of the test failures for listview at the moment look like sequencing errors for messages. I didn't look into it. - The auto mode on Windows 10 looks quite sophisticated, in that it examines many (all) items before deciding the best size. I haven't done anything interesting for auto size mode. - Not really interesting, but the tabs vs spaces situation in this file is a mess. Next Steps ========== - ~~Test with image lists loaded as well~~ - Hope for feedback here (particularly for messages) - Think about auto sizing. - Think about vertical centering within the space. Windows seems to have this as an option, though I haven't discovered how that works just yet. If Merged ========= Now that I have a pretty good grasp of how this component works and lays things out, once this is merged I'll see if I can do Groups as well. Pictures: ======== Windows ListView with Tile mode active. The first entry uses a Callback for the text and the columns layout, while the second is static. {width=300} Wine (with an image list as well) {width=300} And Explorer++ {width=400} Known Issues: ============= - Some highlighting seems a bit janky, I've a quick fix for this, but I think it would be best handled along with: - Hit testing when sub items are touched seems janky too. Test Program =========== ```C #include <windows.h> #include <commctrl.h> HINSTANCE g_hInst; HWND g_hLV; static void AddItems(HWND hLV) { static UINT puColumns[2] = {1,2}; LVITEMW it = {0}; it.mask = LVIF_TEXT | LVIF_COLUMNS; it.cColumns = I_COLUMNSCALLBACK; it.iItem = 0; it.iSubItem = 0; it.pszText = LPSTR_TEXTCALLBACK; ListView_InsertItem(hLV, &it); ListView_SetItemText(hLV, 0, 1, L"Static"); ListView_SetItemText(hLV, 0, 2, L"1 KB"); ListView_SetItemText(hLV, 0, 3, L"2026-02-23 12:00"); it.cColumns = 2; it.puColumns = puColumns; it.iItem = 1; it.iSubItem = 0; it.pszText = L"bravo.txt"; ListView_InsertItem(hLV, &it); ListView_SetItemText(hLV, 1, 1, L"Text"); ListView_SetItemText(hLV, 1, 2, L"12 KB"); ListView_SetItemText(hLV, 1, 3, L"2026-02-22 09:30"); } static void AddColumns(HWND hLV) { LVCOLUMNW col = {0}; col.mask = LVCF_TEXT | LVCF_WIDTH; col.cx = 160; col.pszText = L"Name"; ListView_InsertColumn(hLV, 0, &col); col.cx = 140; col.pszText = L"Type"; ListView_InsertColumn(hLV, 1, &col); col.cx = 100; col.pszText = L"Size"; ListView_InsertColumn(hLV, 2, &col); col.cx = 180; col.pszText = L"Modified"; ListView_InsertColumn(hLV, 3, &col); } static void EnableTileView(HWND hLV) { SIZE size = { 100, 70 }; LVTILEVIEWINFO tileViewInfo = {0}; tileViewInfo.cbSize = sizeof(tileViewInfo); tileViewInfo.dwFlags = LVTVIF_FIXEDSIZE; tileViewInfo.dwMask = LVTVIM_COLUMNS | LVTVIM_TILESIZE; tileViewInfo.cLines = 3; tileViewInfo.sizeTile = size; ListView_SetTileViewInfo(hLV, &tileViewInfo); ListView_SetView(hLV, LV_VIEW_TILE); } static void OnGetDispInfo(NMLVDISPINFO* pnmv) { if (pnmv->item.iItem < 0) { return; } if (pnmv->item.mask & LVIF_TEXT) { // Demonstrates that Windows is passing us a size 20 buffer to write to. pnmv->item.pszText = pnmv->item.cColumns == 20 ? L"Yes" : L"No"; } if (pnmv->item.mask & LVIF_IMAGE) { pnmv->item.iImage = -1; } if (pnmv->item.mask & LVIF_STATE) { pnmv->item.state = 0; } if (pnmv->item.mask & LVIF_COLUMNS) { pnmv->item.cColumns = 2; pnmv->item.puColumns[0] = 1; pnmv->item.puColumns[1] = 2; } } static LRESULT OnNotify(WPARAM wParam, LPARAM lParam) { switch (((LPNMHDR)lParam)->code) { case LVN_GETDISPINFO: OnGetDispInfo((NMLVDISPINFO*) lParam); break; } return TRUE; } static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CREATE: { g_hLV = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"", WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_ALIGNTOP | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_AUTOARRANGE, 0, 0, 0, 0, hWnd, (HMENU)1, g_hInst, NULL); ListView_SetExtendedListViewStyle(g_hLV, LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP); AddColumns(g_hLV); AddItems(g_hLV); EnableTileView(g_hLV); return 0; } case WM_SIZE: if (g_hLV) MoveWindow(g_hLV, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; case WM_NOTIFY: OnNotify(wParam, lParam); return 0; } return DefWindowProcW(hWnd, msg, wParam, lParam); } int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) { INITCOMMONCONTROLSEX iccx; g_hInst = hInstance; /* Initialize common controls */ iccx.dwSize = sizeof(iccx); iccx.dwICC = ICC_STANDARD_CLASSES | ICC_WIN95_CLASSES; InitCommonControlsEx(&iccx); WNDCLASSW wc = {0}; wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.lpszClassName = L"WineLvTileSmoke"; wc.hCursor = LoadCursor(NULL, IDC_ARROW); RegisterClassW(&wc); HWND hWnd = CreateWindowExW(0, wc.lpszClassName, L"ListView Tile puColumns Smoke Test", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 700, 450, NULL, NULL, hInstance, NULL); ShowWindow(hWnd, nCmdShow); MSG msg; while (GetMessageW(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessageW(&msg); } return (int)msg.wParam; } ``` -- v18: comctl32: Implement Tile view style https://gitlab.winehq.org/wine/wine/-/merge_requests/10191
From: Huw Campbell <huw.campbell@gmail.com> --- dlls/comctl32/listview.c | 437 ++++++++++++++++++++++++++++----- dlls/comctl32/tests/listview.c | 208 ++++++++++++++++ 2 files changed, 579 insertions(+), 66 deletions(-) diff --git a/dlls/comctl32/listview.c b/dlls/comctl32/listview.c index f91e63f1f5b..e372c642a39 100644 --- a/dlls/comctl32/listview.c +++ b/dlls/comctl32/listview.c @@ -100,8 +100,6 @@ * -- LVM_GETNUMBEROFWORKAREAS * -- LVM_GETOUTLINECOLOR, LVM_SETOUTLINECOLOR * -- LVM_GETISEARCHSTRINGW, LVM_GETISEARCHSTRINGA - * -- LVM_GETTILEINFO, LVM_SETTILEINFO - * -- LVM_GETTILEVIEWINFO, LVM_SETTILEVIEWINFO * -- LVM_GETWORKAREAS, LVM_SETWORKAREAS * -- LVM_HASGROUP, LVM_INSERTGROUP, LVM_REMOVEGROUP, LVM_REMOVEALLGROUPS * -- LVM_INSERTGROUPSORTED @@ -172,6 +170,8 @@ typedef struct tagITEM_INFO LPARAM lParam; INT iIndent; ITEM_ID *id; + UINT cColumns; + PUINT puColumns; } ITEM_INFO; struct tagITEM_ID @@ -253,6 +253,11 @@ typedef struct tagLISTVIEW_INFO INT nItemHeight; INT nItemWidth; + /* tile settings */ + INT tileWidth; + INT tileHeight; + INT tileLines; + /* style */ DWORD dwStyle; /* the cached window GWL_STYLE */ DWORD dwLvExStyle; /* extended listview style */ @@ -301,6 +306,7 @@ typedef struct tagLISTVIEW_INFO HFONT hDefaultFont; HFONT hFont; INT ntmHeight; /* Some cached metrics of the font used */ + INT ntmExternalLeading; /* Some cached metrics of the font used */ INT ntmMaxCharWidth; /* by the listview to draw items */ INT nEllipsisWidth; @@ -397,6 +403,8 @@ typedef struct tagLISTVIEW_INFO #define LV_ML_DT_FLAGS (DT_TOP | DT_NOPREFIX | DT_EDITCONTROL | DT_CENTER | DT_WORDBREAK | DT_WORD_ELLIPSIS | DT_END_ELLIPSIS) #define LV_FL_DT_FLAGS (DT_TOP | DT_NOPREFIX | DT_EDITCONTROL | DT_CENTER | DT_WORDBREAK | DT_NOCLIP) #define LV_SL_DT_FLAGS (DT_VCENTER | DT_NOPREFIX | DT_EDITCONTROL | DT_SINGLELINE | DT_WORD_ELLIPSIS | DT_END_ELLIPSIS) +/* DrawText flags for Tile View */ +#define LV_TL_DT_FLAGS (DT_TOP | DT_NOPREFIX | DT_EDITCONTROL | DT_SINGLELINE | DT_WORD_ELLIPSIS | DT_END_ELLIPSIS) /* Image index from state */ #define STATEIMAGEINDEX(x) (((x) & LVIS_STATEIMAGEMASK) >> 12) @@ -538,11 +546,52 @@ static inline int textcmpWT(LPCWSTR aw, LPCWSTR bt, BOOL isW) int r = bw ? lstrcmpW(aw, bw) : 1; textfreeT(bw, isW); return r; - } - + } + return 1; } + +/******** PUColumn ownership function *************************************/ + +/* The list view owns puColumn data arrays. */ + +/* Helper function to do the allocation */ +static inline UINT* puColumnsDup(UINT cColumns, UINT* puColumns) +{ + UINT* ret = Alloc(cColumns * sizeof(UINT)); + if (ret) memcpy(ret, puColumns, cColumns * sizeof(UINT)); + return ret; +} + +/* + * Dest is a pointer to a managed puColumns we are copying an owned copy to. + * Src is an existing puColumns passed from the user. + * + * If cColumns is I_COLUMNSCALLBACK, it won't be in the range and we won't + * try to copy from the pointer. + */ +static inline void puColumnsSetPtr(UINT **dest, UINT* src, UINT cColumns) +{ + if (*dest) Free(*dest); + if (src && cColumns > 0 && cColumns < 20) + *dest = puColumnsDup(cColumns, src); + else + *dest = NULL; +} + +/* + * Compare two puColumn entries for equality when their cColumns entries + * have the same value. + */ +static inline INT puColumnsCmp(PUINT at, PUINT bt, UINT cColumns) +{ + if (!cColumns || cColumns == I_COLUMNSCALLBACK) return 0; + if (!at) return bt ? -1 : 0; + if (!bt) return 1; + return memcmp(at, bt, sizeof(UINT) * cColumns); +} + /******** Debugging functions *****************************************/ static inline LPCSTR debugtext_t(LPCWSTR text, BOOL isW) @@ -1054,6 +1103,10 @@ static int notify_odfinditem(const LISTVIEW_INFO *infoPtr, NMLVFINDITEMW *nmlv) return ret; } +static inline BOOL is_icon_like(const LISTVIEW_INFO *infoPtr) { + return infoPtr->uView == LV_VIEW_ICON || infoPtr->uView == LV_VIEW_SMALLICON || infoPtr->uView == LV_VIEW_TILE; +} + static void customdraw_fill(NMLVCUSTOMDRAW *lpnmlvcd, const LISTVIEW_INFO *infoPtr, HDC hdc, const RECT *rcBounds, const LVITEMW *lplvItem) { @@ -1110,9 +1163,13 @@ static void prepaint_setup (const LISTVIEW_INFO *infoPtr, HDC hdc, const NMLVCUS } } + /* Subitems in Tile mode are drawn gray if the colour is set to the default; + * otherwise, the colour of subitems is the same as the main text. */ if (backcolor == CLR_DEFAULT) backcolor = comctl32_color.clrWindow; - if (textcolor == CLR_DEFAULT) + if (textcolor == CLR_DEFAULT && infoPtr->uView == LV_VIEW_TILE && SubItem) + textcolor = comctl32_color.clrGrayText; + else if (textcolor == CLR_DEFAULT) textcolor = comctl32_color.clrWindowText; /* Set the text attributes */ @@ -1373,10 +1430,10 @@ static BOOL iterator_frameditems_absolute(ITERATOR* i, const LISTVIEW_INFO* info if (infoPtr->nItemCount == 0) return TRUE; - if (infoPtr->uView == LV_VIEW_ICON || infoPtr->uView == LV_VIEW_SMALLICON) + if (is_icon_like(infoPtr)) { INT nItem; - + if (infoPtr->uView == LV_VIEW_ICON && infoPtr->nFocusedItem != -1) { LISTVIEW_GetItemBox(infoPtr, infoPtr->nFocusedItem, &rcItem); @@ -1603,8 +1660,7 @@ static inline LRESULT CallWindowProcT(WNDPROC proc, HWND hwnd, UINT uMsg, static inline BOOL is_autoarrange(const LISTVIEW_INFO *infoPtr) { - return (infoPtr->dwStyle & LVS_AUTOARRANGE) && - (infoPtr->uView == LV_VIEW_ICON || infoPtr->uView == LV_VIEW_SMALLICON); + return (infoPtr->dwStyle & LVS_AUTOARRANGE) && is_icon_like(infoPtr); } static void toggle_checkbox_state(LISTVIEW_INFO *infoPtr, INT nItem) @@ -1767,9 +1823,9 @@ static inline void LISTVIEW_InvalidateSubItem(const LISTVIEW_INFO *infoPtr, INT { POINT Origin, Position; RECT rcBox; - - if(!is_redrawing(infoPtr)) return; - assert (infoPtr->uView == LV_VIEW_DETAILS); + + if(!is_redrawing(infoPtr)) return; + assert (infoPtr->uView == LV_VIEW_DETAILS || infoPtr->uView == LV_VIEW_TILE); LISTVIEW_GetOrigin(infoPtr, &Origin); LISTVIEW_GetItemOrigin(infoPtr, nItem, &Position); LISTVIEW_GetHeaderRect(infoPtr, nSubItem, &rcBox); @@ -2063,7 +2119,7 @@ static INT LISTVIEW_UpdateHScroll(LISTVIEW_INFO *infoPtr) { horzInfo.nMax = infoPtr->nItemWidth; } - else /* LV_VIEW_ICON, or LV_VIEW_SMALLICON */ + else /* LV_VIEW_ICON, or LV_VIEW_SMALLICON or LV_VIEW_TILE */ { RECT rcView; @@ -2287,7 +2343,7 @@ static void LISTVIEW_GetItemOrigin(const LISTVIEW_INFO *infoPtr, INT nItem, LPPO { assert(nItem >= 0 && nItem < infoPtr->nItemCount); - if ((infoPtr->uView == LV_VIEW_SMALLICON) || (infoPtr->uView == LV_VIEW_ICON)) + if (is_icon_like(infoPtr)) { lpptPosition->x = (LONG_PTR)DPA_GetPtr(infoPtr->hdpaPosX, nItem); lpptPosition->y = (LONG_PTR)DPA_GetPtr(infoPtr->hdpaPosY, nItem); @@ -2362,7 +2418,7 @@ static void LISTVIEW_GetItemMetrics(const LISTVIEW_INFO *infoPtr, const LVITEMW TRACE("(lpLVItem=%s)\n", debuglvitem_t(lpLVItem, TRUE)); /* Be smart and try to figure out the minimum we have to do */ - if (lpLVItem->iSubItem) assert(infoPtr->uView == LV_VIEW_DETAILS); + if (lpLVItem->iSubItem) assert(infoPtr->uView == LV_VIEW_DETAILS || infoPtr->uView == LV_VIEW_TILE); if (infoPtr->uView == LV_VIEW_ICON && (lprcBox || lprcLabel)) { assert((lpLVItem->mask & LVIF_STATE) && (lpLVItem->stateMask & LVIS_FOCUSED)); @@ -2380,10 +2436,10 @@ static void LISTVIEW_GetItemMetrics(const LISTVIEW_INFO *infoPtr, const LVITEMW /************************************************************/ /* compute the box rectangle (it should be cheap to do) */ /************************************************************/ - if (lpLVItem->iSubItem || infoPtr->uView == LV_VIEW_DETAILS) + if (infoPtr->uView == LV_VIEW_DETAILS) lpColumnInfo = LISTVIEW_GetColumnInfo(infoPtr, lpLVItem->iSubItem); - if (lpLVItem->iSubItem) + if (lpLVItem->iSubItem && infoPtr->uView == LV_VIEW_DETAILS) { Box = lpColumnInfo->rcHeader; } @@ -2419,6 +2475,18 @@ static void LISTVIEW_GetItemMetrics(const LISTVIEW_INFO *infoPtr, const LVITEMW Icon.bottom += infoPtr->iconSize.cy; } } + else if (infoPtr->uView == LV_VIEW_TILE) + { + Icon.left = Box.left; + Icon.top = Box.top; + Icon.right = Icon.left; + Icon.bottom = Icon.top; + if (infoPtr->himlNormal) + { + Icon.right += infoPtr->iconSize.cx; + Icon.bottom += infoPtr->iconSize.cy; + } + } else /* LV_VIEW_SMALLICON, LV_VIEW_LIST or LV_VIEW_DETAILS */ { Icon.left = Box.left + state_width; @@ -2470,13 +2538,14 @@ static void LISTVIEW_GetItemMetrics(const LISTVIEW_INFO *infoPtr, const LVITEMW } } - if (lpLVItem->iSubItem || ((infoPtr->dwStyle & LVS_OWNERDRAWFIXED) && infoPtr->uView == LV_VIEW_DETAILS)) + /* for details mode we can use the column size for the label size. */ + if (infoPtr->uView == LV_VIEW_DETAILS && (lpLVItem->iSubItem || (infoPtr->dwStyle & LVS_OWNERDRAWFIXED))) { labelSize.cx = infoPtr->nItemWidth; labelSize.cy = infoPtr->nItemHeight; goto calc_label; } - + /* we need the text in non owner draw mode */ assert(lpLVItem->mask & LVIF_TEXT); if (is_text(lpLVItem->pszText)) @@ -2497,9 +2566,11 @@ static void LISTVIEW_GetItemMetrics(const LISTVIEW_INFO *infoPtr, const LVITEMW /* now figure out the flags */ if (infoPtr->uView == LV_VIEW_ICON) uFormat = oversizedBox ? LV_FL_DT_FLAGS : LV_ML_DT_FLAGS; + if (infoPtr->uView == LV_VIEW_TILE) + uFormat = LV_TL_DT_FLAGS; else uFormat = LV_SL_DT_FLAGS; - + DrawTextW (hdc, lpLVItem->pszText, -1, &rcText, uFormat | DT_CALCRECT); if (rcText.right != rcText.left) @@ -2528,6 +2599,29 @@ calc_label: } Label.bottom = Label.top + labelSize.cy + HEIGHT_PADDING; } + else if (infoPtr->uView == LV_VIEW_TILE) + { + Label.left = Icon.right; + if (lpLVItem->iSubItem == 0) { + Label.top = Box.top; + Label.right = Label.left + labelSize.cx; + Label.bottom = Label.top + labelSize.cy;; + } + else if (infoPtr->tileLines && lpLVItem->cColumns) { + /* Cludgy side channel. Subitems can't have cColumns; and the fetch won't overwrite it, so put + * the index of the sub-item into the cColumns field. */ + Label.top = Box.top + lpLVItem->cColumns * (infoPtr->ntmHeight + infoPtr->ntmExternalLeading); + Label.right = Label.left + labelSize.cx; + Label.bottom = Label.top + labelSize.cy; + + /* Make sure we're not overflowing the box, if items don't fit it's ok to not draw them. */ + if (Label.bottom > Box.bottom) + Label.bottom = Label.top; + } else { + Label.top = Label.bottom = Box.top; + Label.right = Label.left; + } + } else if (infoPtr->uView == LV_VIEW_DETAILS) { Label.left = Icon.right; @@ -2806,8 +2900,8 @@ static BOOL LISTVIEW_Arrange(LISTVIEW_INFO *infoPtr, INT nAlignCode) POINT pos; INT i; - if (infoPtr->uView != LV_VIEW_ICON && infoPtr->uView != LV_VIEW_SMALLICON) return FALSE; - + if (!is_icon_like(infoPtr)) return FALSE; + TRACE("nAlignCode=%d\n", nAlignCode); if (nAlignCode == LVA_DEFAULT) @@ -2857,6 +2951,7 @@ static void LISTVIEW_GetAreaRect(const LISTVIEW_INFO *infoPtr, LPRECT lprcView) { case LV_VIEW_ICON: case LV_VIEW_SMALLICON: + case LV_VIEW_TILE: for (i = 0; i < infoPtr->nItemCount; i++) { x = (LONG_PTR)DPA_GetPtr(infoPtr->hdpaPosX, i); @@ -2975,6 +3070,12 @@ static INT LISTVIEW_CalculateItemWidth(const LISTVIEW_INFO *infoPtr) nItemWidth = rcHeader.right; } } + else if (infoPtr->uView == LV_VIEW_TILE) { + if (infoPtr->tileWidth > 0) + nItemWidth = infoPtr->tileWidth; + else + nItemWidth = infoPtr->iconSize.cx + ICON_LR_PADDING + DEFAULT_COLUMN_WIDTH; + } else /* LV_VIEW_SMALLICON, or LV_VIEW_LIST */ { WCHAR szDispText[DISP_TEXT_SIZE] = { '\0' }; @@ -3021,6 +3122,12 @@ static INT LISTVIEW_CalculateItemHeight(const LISTVIEW_INFO *infoPtr) if (infoPtr->uView == LV_VIEW_ICON) nItemHeight = infoPtr->iconSpacing.cy; + else if (infoPtr->uView == LV_VIEW_TILE) { + if (infoPtr->tileHeight > 0) + nItemHeight = max(infoPtr->ntmHeight, infoPtr->tileHeight); + else + nItemHeight = max(infoPtr->ntmHeight, infoPtr->iconSize.cy) + HEIGHT_PADDING; + } else { nItemHeight = infoPtr->ntmHeight; @@ -4282,9 +4389,14 @@ static BOOL set_main_item(LISTVIEW_INFO *infoPtr, const LVITEMW *lpLVItem, BOOL if ((lpLVItem->mask & LVIF_TEXT) && textcmpWT(lpItem->hdr.pszText, lpLVItem->pszText, isW)) uChanged |= LVIF_TEXT; - + + if (lpLVItem->mask & LVIF_COLUMNS && ( + lpItem->cColumns != lpLVItem->cColumns || puColumnsCmp(lpItem->puColumns, lpLVItem->puColumns, lpItem->cColumns)) + ) + uChanged |= LVIF_COLUMNS; + TRACE("change mask=0x%x\n", uChanged); - + memset(&nmlv, 0, sizeof(NMLISTVIEW)); nmlv.iItem = lpLVItem->iItem; if (lpLVItem->mask & LVIF_STATE) @@ -4339,6 +4451,11 @@ static BOOL set_main_item(LISTVIEW_INFO *infoPtr, const LVITEMW *lpLVItem, BOOL if (lpLVItem->mask & LVIF_INDENT) lpItem->iIndent = lpLVItem->iIndent; + if (lpLVItem->mask & LVIF_COLUMNS) { + lpItem->cColumns = lpLVItem->cColumns; + puColumnsSetPtr(&lpItem->puColumns, lpLVItem->puColumns, lpLVItem->cColumns); + } + if (uChanged & LVIF_STATE) { if (lpItem && (stateMask & ~infoPtr->uCallbackMask)) @@ -4546,6 +4663,101 @@ static BOOL LISTVIEW_SetItemT(LISTVIEW_INFO *infoPtr, LVITEMW *lpLVItem, BOOL is return bResult; } +/*** + * DESCRIPTION: + * Gets the Tile Info for an item in the list. + * + * PARAMETER(S): + * [I] hwnd : window handle + * [I] lParam : tile info data + * + * RETURN Not Used + */ +static int LISTVIEW_GetTileInfo(const LISTVIEW_INFO *infoPtr, PLVTILEINFO lParam) { + LVITEMW item = {0}; + item.iItem = lParam->iItem; + item.mask = LVIF_COLUMNS; + item.cColumns = lParam->cColumns; + item.puColumns = lParam->puColumns; + LISTVIEW_GetItemW(infoPtr, &item); + /* puColumns is a pointer and isn't changed, but we need to copy back the size. */ + lParam->cColumns = item.cColumns; + return TRUE; +} + +/*** + * DESCRIPTION: + * Gets the Tile Info for an item in the list. + * + * PARAMETER(S): + * [I] hwnd : window handle + * [I] lParam : tile info data + * + * RETURN Not Used + */ +static int LISTVIEW_GetTileViewInfo(const LISTVIEW_INFO *infoPtr, PLVTILEVIEWINFO lParam) { + if (lParam->dwMask & LVTVIM_TILESIZE) { + lParam->sizeTile.cx = infoPtr->tileWidth; + lParam->sizeTile.cy = infoPtr->tileHeight; + } + if (lParam->dwMask & LVTVIM_COLUMNS) { + lParam->cLines = infoPtr->tileLines; + } + + return TRUE; +} + +/*** + * DESCRIPTION: + * Sets the Tile Info for an item in the list. + * + * PARAMETER(S): + * [I] hwnd : window handle + * [I] lParam : tile info data + * + * RETURN: + * SUCCESS : TRUE + * FAILURE : FALSE + */ +static BOOL LISTVIEW_SetTileInfo(LISTVIEW_INFO *infoPtr, PLVTILEINFO lParam) +{ + LVITEMW lvItem = {0}; + if (lParam->iItem < 0 || lParam->iItem >= infoPtr->nItemCount) return FALSE; + if (infoPtr->dwStyle & LVS_OWNERDATA) return FALSE; + + lvItem.iItem = lParam->iItem; + lvItem.mask = LVIF_COLUMNS; + lvItem.cColumns = lParam->cColumns; + lvItem.puColumns = lParam->puColumns; + + TRACE("(nItem=%d, lpLVItem=%s, isW=%d)\n", lParam->iItem, debuglvitem_t(&lvItem, TRUE), TRUE); + + return LISTVIEW_SetItemT(infoPtr, &lvItem, TRUE); +} + + +/*** + * DESCRIPTION: + * Gets the Tile Info for an item in the list. + * + * PARAMETER(S): + * [I] hwnd : window handle + * [I] lParam : tile info data + * + * RETURN Not Used + */ +static int LISTVIEW_SetTileViewInfo(LISTVIEW_INFO *infoPtr, PLVTILEVIEWINFO lParam) { + if (lParam->dwMask & LVTVIM_TILESIZE) { + infoPtr->tileWidth = lParam->dwFlags & LVTVIF_FIXEDWIDTH ? lParam->sizeTile.cx : 0; + infoPtr->tileHeight = lParam->dwFlags & LVTVIF_FIXEDHEIGHT ? lParam->sizeTile.cy : 0; + } + if (lParam->dwMask & LVTVIM_COLUMNS) { + infoPtr->tileLines = lParam->cLines; + } + + return TRUE; +} + /*** * DESCRIPTION: * Retrieves the index of the item at coordinate (0, 0) of the client area. @@ -4635,6 +4847,7 @@ static void LISTVIEW_DrawItemPart(LISTVIEW_INFO *infoPtr, LVITEMW *item, const N HIMAGELIST himl; UINT format; RECT *focus; + BOOL isTileSubitem = infoPtr->uView == LV_VIEW_TILE && item->iSubItem; /* now check if we need to update the focus rectangle */ focus = infoPtr->bFocus && (item->state & LVIS_FOCUSED) ? &infoPtr->rcFocus : 0; @@ -4667,7 +4880,9 @@ static void LISTVIEW_DrawItemPart(LISTVIEW_INFO *infoPtr, LVITEMW *item, const N /* in detail mode, we want to paint background for label rect when * item is not selected or listview has full row select; otherwise paint * background for text only */ - if ( infoPtr->uView == LV_VIEW_ICON || + /* finally, for tile subitems, we don't want to blat other labels in the + * select area, just this label's space. */ + if ( infoPtr->uView == LV_VIEW_ICON || isTileSubitem || (infoPtr->uView == LV_VIEW_DETAILS && (!(item->state & LVIS_SELECTED) || (infoPtr->dwLvExStyle & LVS_EX_FULLROWSELECT)))) background = &rcLabel; @@ -4703,6 +4918,10 @@ static void LISTVIEW_DrawItemPart(LISTVIEW_INFO *infoPtr, LVITEMW *item, const N } infoPtr->rcFocus = rcSelect; } + else if (isTileSubitem) + /* in tile mode the focus box wraps across all the labels; as the subitems are drawn after + * the main item, that means we can take a union here. */ + UnionRect(&infoPtr->rcFocus, &infoPtr->rcFocus, &rcLabel); else infoPtr->rcFocus = rcLabel; } @@ -4719,8 +4938,9 @@ static void LISTVIEW_DrawItemPart(LISTVIEW_INFO *infoPtr, LVITEMW *item, const N } /* item icons */ - himl = (infoPtr->uView == LV_VIEW_ICON ? infoPtr->himlNormal : infoPtr->himlSmall); - if (himl && item->iImage >= 0 && !IsRectEmpty(&rcIcon)) + himl = ((infoPtr->uView == LV_VIEW_ICON || infoPtr->uView == LV_VIEW_TILE) ? infoPtr->himlNormal : infoPtr->himlSmall); + /* draw the icon if it exists and we're not the subitem in tile mode (the main item will draw it) */ + if (himl && item->iImage >= 0 && !IsRectEmpty(&rcIcon) && !isTileSubitem) { UINT style; @@ -4741,10 +4961,14 @@ static void LISTVIEW_DrawItemPart(LISTVIEW_INFO *infoPtr, LVITEMW *item, const N if (infoPtr->hwndEdit && item->iItem == infoPtr->nEditLabelItem && item->iSubItem == 0) return; /* figure out the text drawing flags */ - format = (infoPtr->uView == LV_VIEW_ICON ? (focus ? LV_FL_DT_FLAGS : LV_ML_DT_FLAGS) : LV_SL_DT_FLAGS); if (infoPtr->uView == LV_VIEW_ICON) - format = (focus ? LV_FL_DT_FLAGS : LV_ML_DT_FLAGS); - else if (item->iSubItem) + format = focus ? LV_FL_DT_FLAGS : LV_ML_DT_FLAGS; + if (infoPtr->uView == LV_VIEW_TILE) + format = LV_TL_DT_FLAGS; + else + format = LV_SL_DT_FLAGS; + + if (infoPtr->uView == LV_VIEW_DETAILS && item->iSubItem) { switch (LISTVIEW_GetColumnInfo(infoPtr, item->iSubItem)->fmt & LVCFMT_JUSTIFYMASK) { @@ -4790,20 +5014,25 @@ static BOOL LISTVIEW_DrawItem(LISTVIEW_INFO *infoPtr, HDC hdc, INT nItem, ITERAT DWORD cdsubitemmode = CDRF_DODEFAULT; RECT *focus, rcBox; NMLVCUSTOMDRAW nmlvcd; - LVITEMW lvItem; + LVITEMW lvItem = {0}; + UINT puColumns[20] = {0}; + INT numTileLines; TRACE("(hdc=%p, nItem=%d, subitems=%p, pos=%s)\n", hdc, nItem, subitems, wine_dbgstr_point(&pos)); /* get information needed for drawing the item */ lvItem.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM | LVIF_STATE; if (infoPtr->uView == LV_VIEW_DETAILS) lvItem.mask |= LVIF_INDENT; + if (infoPtr->uView == LV_VIEW_TILE) { + lvItem.mask |= LVIF_COLUMNS; + lvItem.cColumns = 20; + lvItem.puColumns = puColumns; + } lvItem.stateMask = LVIS_SELECTED | LVIS_FOCUSED | LVIS_STATEIMAGEMASK | LVIS_CUT | LVIS_OVERLAYMASK; lvItem.iItem = nItem; - lvItem.iSubItem = 0; - lvItem.state = 0; - lvItem.lParam = 0; lvItem.cchTextMax = DISP_TEXT_SIZE; lvItem.pszText = szDispText; + if (!LISTVIEW_GetItemW(infoPtr, &lvItem)) return FALSE; if (lvItem.pszText == LPSTR_TEXTCALLBACKW) lvItem.pszText = callbackW; TRACE(" lvItem=%s\n", debuglvitem_t(&lvItem, TRUE)); @@ -4856,7 +5085,7 @@ static BOOL LISTVIEW_DrawItem(LISTVIEW_INFO *infoPtr, HDC hdc, INT nItem, ITERAT szDispText[0] = 0; if (!LISTVIEW_GetItemW(infoPtr, &lvItem)) return FALSE; if (infoPtr->dwLvExStyle & LVS_EX_FULLROWSELECT) - lvItem.state = LISTVIEW_GetItemState(infoPtr, nItem, LVIS_SELECTED); + lvItem.state = LISTVIEW_GetItemState(infoPtr, nItem, LVIS_SELECTED); if (lvItem.pszText == LPSTR_TEXTCALLBACKW) lvItem.pszText = callbackW; TRACE(" lvItem=%s\n", debuglvitem_t(&lvItem, TRUE)); @@ -4885,6 +5114,41 @@ static BOOL LISTVIEW_DrawItem(LISTVIEW_INFO *infoPtr, HDC hdc, INT nItem, ITERAT { prepaint_setup(infoPtr, hdc, &nmlvcd, FALSE); LISTVIEW_DrawItemPart(infoPtr, &lvItem, &nmlvcd, &pos); + + /* Tile mode handling of subitems. + * + * Handle subitem and rendering here, as the call for DrawItems (LISTVIEW_RefreshList) didn't + * yet have access to the puColumns array, so didn't directly know which columns to render and + * in which order. + * + * Reports mode is a bit different there, as there can be many columns, so it has to be careful to + * only pull the few required, as many could be off screen. + * + * NOTE: TileView Positions + * Because these are laid out in order, and their displacement depends on those preceding them, + * we pack some side channel information into lvItem.cColumns. + */ + + if (infoPtr->uView == LV_VIEW_TILE && infoPtr->tileLines) + { + lvItem.mask = LVIF_TEXT; + lvItem.puColumns = NULL; + + numTileLines = min(infoPtr->tileLines, lvItem.cColumns); + for (UINT subIx = 0; subIx < numTileLines; subIx++) + { + lvItem.cColumns = subIx + 1; + lvItem.iSubItem = puColumns[subIx]; + lvItem.cchTextMax = DISP_TEXT_SIZE; + lvItem.pszText = szDispText; + szDispText[0] = 0; + + if (LISTVIEW_GetItemW(infoPtr, &lvItem)) { + prepaint_setup(infoPtr, hdc, &nmlvcd, TRUE); + LISTVIEW_DrawItemPart(infoPtr, &lvItem, &nmlvcd, &pos); + } + } + } } postpaint: @@ -5599,6 +5863,7 @@ static BOOL LISTVIEW_DeleteAllItems(LISTVIEW_INFO *infoPtr, BOOL destroy) if (is_text(hdrItem->pszText)) Free(hdrItem->pszText); Free(hdrItem); } + if (lpItem->puColumns) Free(lpItem->puColumns); DPA_Destroy(hdpaSubItems); DPA_DeletePtr(infoPtr->hdpaItems, i); } @@ -5855,13 +6120,13 @@ static void LISTVIEW_ScrollOnInsert(LISTVIEW_INFO *infoPtr, INT nItem, INT dir) static BOOL LISTVIEW_DeleteItem(LISTVIEW_INFO *infoPtr, INT nItem) { LVITEMW item; - const BOOL is_icon = (infoPtr->uView == LV_VIEW_SMALLICON || infoPtr->uView == LV_VIEW_ICON); + const BOOL is_icon = is_icon_like(infoPtr); INT focus = infoPtr->nFocusedItem; TRACE("(nItem=%d)\n", nItem); if (nItem < 0 || nItem >= infoPtr->nItemCount) return FALSE; - + /* remove selection, and focus */ item.state = 0; item.stateMask = LVIS_SELECTED | LVIS_FOCUSED; @@ -5896,6 +6161,7 @@ static BOOL LISTVIEW_DeleteItem(LISTVIEW_INFO *infoPtr, INT nItem) if (is_text(hdrItem->pszText)) Free(hdrItem->pszText); Free(hdrItem); } + if (lpItem->puColumns) Free(lpItem->puColumns); DPA_Destroy(hdpaSubItems); } @@ -6824,6 +7090,18 @@ static BOOL LISTVIEW_GetItemT(const LISTVIEW_INFO *infoPtr, LPLVITEMW lpLVItem, dispInfo.item.iIndent = I_INDENTCALLBACK; } + /* Only items support tileview columns descriptions */ + if ((lpLVItem->mask & LVIF_COLUMNS) && lpItem->cColumns == I_COLUMNSCALLBACK && + !lpLVItem->iSubItem && lpLVItem->cColumns <= 20) + { + /* Max size of cColumns is 20, allocate a buffer of the requested size to pass to the + * application. Its job is to set the max to the correct number and put the items into, + * this buffer, while we retain ownership of it. */ + dispInfo.item.mask |= LVIF_COLUMNS; + dispInfo.item.cColumns = lpLVItem->cColumns; + dispInfo.item.puColumns = Alloc(lpLVItem->cColumns * sizeof(UINT)); + } + /* Apps depend on calling back for text if it is NULL or LPSTR_TEXTCALLBACKW */ if ((lpLVItem->mask & LVIF_TEXT) && !(lpLVItem->mask & LVIF_NORECOMPUTE) && !is_text(pItemHdr->pszText)) @@ -6898,21 +7176,21 @@ static BOOL LISTVIEW_GetItemT(const LISTVIEW_INFO *infoPtr, LPLVITEMW lpLVItem, lpLVItem->state &= ~dispInfo.item.stateMask; lpLVItem->state |= (dispInfo.item.state & dispInfo.item.stateMask); } - if ( lpLVItem->stateMask & ~infoPtr->uCallbackMask & LVIS_FOCUSED ) + if ( lpLVItem->stateMask & ~infoPtr->uCallbackMask & LVIS_FOCUSED ) { lpLVItem->state &= ~LVIS_FOCUSED; if (infoPtr->nFocusedItem == lpLVItem->iItem) lpLVItem->state |= LVIS_FOCUSED; } - if ( lpLVItem->stateMask & ~infoPtr->uCallbackMask & LVIS_SELECTED ) + if ( lpLVItem->stateMask & ~infoPtr->uCallbackMask & LVIS_SELECTED ) { lpLVItem->state &= ~LVIS_SELECTED; if (ranges_contain(infoPtr->selectionRanges, lpLVItem->iItem)) lpLVItem->state |= LVIS_SELECTED; - } + } } - /* and last, but not least, the indent field */ + /* Next is the indent field */ if (dispInfo.item.mask & LVIF_INDENT) { lpLVItem->iIndent = dispInfo.item.iIndent; @@ -6924,6 +7202,39 @@ static BOOL LISTVIEW_GetItemT(const LISTVIEW_INFO *infoPtr, LPLVITEMW lpLVItem, lpLVItem->iIndent = lpItem->iIndent; } + /* And then our Column Fields */ + if (dispInfo.item.mask & LVIF_COLUMNS) + { + lpLVItem->cColumns = dispInfo.item.cColumns; + /* The caller owns lpLVItem->puColumns, copy our data into it. */ + if (lpLVItem->puColumns) + memcpy(lpLVItem->puColumns, dispInfo.item.puColumns, dispInfo.item.cColumns * sizeof(UINT)); + + if ((dispInfo.item.mask & LVIF_DI_SETITEM) && lpItem->cColumns == I_COLUMNSCALLBACK) { + lpItem->cColumns = dispInfo.item.cColumns; + + /* We allocated for dispInfo.item.puColumns, free then pass ownership. */ + if (lpItem->puColumns) Free(lpItem->puColumns); + lpItem->puColumns = dispInfo.item.puColumns; + } else { + /* Otherwise its our reponsibility to free it */ + Free(dispInfo.item.puColumns); + } + } + /* When one doesn't use a callback, they can query for the size by passing null for puColumns. */ + else if (lpLVItem->mask & LVIF_COLUMNS) { + if (lpLVItem->puColumns && lpLVItem->cColumns >= lpItem->cColumns) { + lpLVItem->cColumns = lpItem->cColumns; + // If there's no data we won't store an allocation. + if (lpItem->puColumns) + memcpy(lpLVItem->puColumns, lpItem->puColumns, lpItem->cColumns * sizeof(UINT)); + } else if (lpLVItem->puColumns) { + return FALSE; + } else { + lpLVItem->cColumns = lpItem->cColumns; + } + } + return TRUE; } @@ -7744,6 +8055,8 @@ static INT LISTVIEW_HitTest(const LISTVIEW_INFO *infoPtr, LPLVHITTESTINFO lpht, if (infoPtr->dwLvExStyle & LVS_EX_FULLROWSELECT) opt.x = lpht->pt.x - Origin.x; } + else if (infoPtr->uView == LV_VIEW_TILE) + rcBounds = rcBox; else { UnionRect(&rcBounds, &rcIcon, &rcLabel); @@ -7913,7 +8226,7 @@ static INT LISTVIEW_InsertItemT(LISTVIEW_INFO *infoPtr, const LVITEMW *lpLVItem, if (!set_main_item(infoPtr, &item, TRUE, isW, &has_changed)) goto undo; /* make room for the position, if we are in the right mode */ - if ((infoPtr->uView == LV_VIEW_SMALLICON) || (infoPtr->uView == LV_VIEW_ICON)) + if (is_icon_like(infoPtr)) { if (DPA_InsertPtr(infoPtr->hdpaPosX, nItem, 0) == -1) goto undo; @@ -7933,7 +8246,7 @@ static INT LISTVIEW_InsertItemT(LISTVIEW_INFO *infoPtr, const LVITEMW *lpLVItem, return -1; /* align items (set position of each item) */ - if (infoPtr->uView == LV_VIEW_SMALLICON || infoPtr->uView == LV_VIEW_ICON) + if (is_icon_like(infoPtr)) { POINT pt; @@ -9308,17 +9621,14 @@ static BOOL LISTVIEW_SetUnicodeFormat( LISTVIEW_INFO *infoPtr, BOOL unicode) static INT LISTVIEW_SetView(LISTVIEW_INFO *infoPtr, DWORD nView) { HIMAGELIST himl; + BOOL isNormal; if (infoPtr->uView == nView) return 1; if ((INT)nView < 0 || nView > LV_VIEW_MAX) return -1; - if (nView == LV_VIEW_TILE) - { - FIXME("View LV_VIEW_TILE unimplemented\n"); - return -1; - } infoPtr->uView = nView; + isNormal = nView == LV_VIEW_ICON || nView == LV_VIEW_TILE; SendMessageW(infoPtr->hwndEdit, WM_KILLFOCUS, 0, 0); ShowWindow(infoPtr->hwndHeader, SW_HIDE); @@ -9326,16 +9636,10 @@ static INT LISTVIEW_SetView(LISTVIEW_INFO *infoPtr, DWORD nView) ShowScrollBar(infoPtr->hwndSelf, SB_BOTH, FALSE); SetRectEmpty(&infoPtr->rcFocus); - himl = (nView == LV_VIEW_ICON ? infoPtr->himlNormal : infoPtr->himlSmall); - set_icon_size(&infoPtr->iconSize, himl, nView != LV_VIEW_ICON); + himl = isNormal ? infoPtr->himlNormal : infoPtr->himlSmall; + set_icon_size(&infoPtr->iconSize, himl, !isNormal); - switch (nView) - { - case LV_VIEW_ICON: - case LV_VIEW_SMALLICON: - LISTVIEW_Arrange(infoPtr, LVA_DEFAULT); - break; - case LV_VIEW_DETAILS: + if (nView == LV_VIEW_DETAILS) { HDLAYOUT hl; WINDOWPOS wp; @@ -9347,13 +9651,10 @@ static INT LISTVIEW_SetView(LISTVIEW_INFO *infoPtr, DWORD nView) SendMessageW(infoPtr->hwndHeader, HDM_LAYOUT, 0, (LPARAM)&hl); SetWindowPos(infoPtr->hwndHeader, infoPtr->hwndSelf, wp.x, wp.y, wp.cx, wp.cy, wp.flags | ((infoPtr->dwStyle & LVS_NOCOLUMNHEADER) ? SWP_HIDEWINDOW : SWP_SHOWWINDOW)); - break; - } - case LV_VIEW_LIST: - break; } LISTVIEW_UpdateItemSize(infoPtr); + LISTVIEW_Arrange(infoPtr, LVA_DEFAULT); LISTVIEW_UpdateSize(infoPtr); LISTVIEW_UpdateScroll(infoPtr); LISTVIEW_InvalidateList(infoPtr); @@ -10810,7 +11111,7 @@ static LRESULT LISTVIEW_Paint(LISTVIEW_INFO *infoPtr, HDC hdc) { infoPtr->bNoItemMetrics = FALSE; LISTVIEW_UpdateItemSize(infoPtr); - if (infoPtr->uView == LV_VIEW_ICON || infoPtr->uView == LV_VIEW_SMALLICON) + if (is_icon_like(infoPtr)) LISTVIEW_Arrange(infoPtr, LVA_DEFAULT); LISTVIEW_UpdateScroll(infoPtr); } @@ -11612,9 +11913,11 @@ LISTVIEW_WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) case LVM_GETTEXTCOLOR: return infoPtr->clrText; - /* case LVM_GETTILEINFO: */ + case LVM_GETTILEINFO: + return LISTVIEW_GetTileInfo(infoPtr, (PLVTILEINFO)lParam); - /* case LVM_GETTILEVIEWINFO: */ + case LVM_GETTILEVIEWINFO: + return LISTVIEW_GetTileViewInfo(infoPtr, (PLVTILEVIEWINFO)lParam); case LVM_GETTOOLTIPS: if( !infoPtr->hwndToolTip ) @@ -11778,9 +12081,11 @@ LISTVIEW_WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) case LVM_SETTEXTCOLOR: return LISTVIEW_SetTextColor(infoPtr, (COLORREF)lParam); - /* case LVM_SETTILEINFO: */ + case LVM_SETTILEINFO: + return LISTVIEW_SetTileInfo(infoPtr, (PLVTILEINFO)lParam); - /* case LVM_SETTILEVIEWINFO: */ + case LVM_SETTILEVIEWINFO: + return LISTVIEW_SetTileViewInfo(infoPtr, (PLVTILEVIEWINFO)lParam); /* case LVM_SETTILEWIDTH: */ diff --git a/dlls/comctl32/tests/listview.c b/dlls/comctl32/tests/listview.c index 951def8fe0f..777a9dce153 100644 --- a/dlls/comctl32/tests/listview.c +++ b/dlls/comctl32/tests/listview.c @@ -770,6 +770,13 @@ static LRESULT WINAPI parent_wnd_proc(HWND hwnd, UINT message, WPARAM wParam, LP ok(dispinfo->item.cchTextMax == 260 || broken(dispinfo->item.cchTextMax == 264) /* NT4 reports aligned size */, "buffer size %d\n", dispinfo->item.cchTextMax); + + if (dispinfo->item.mask & LVIF_COLUMNS) + { + dispinfo->item.cColumns = 2; + dispinfo->item.puColumns[0] = 7; + dispinfo->item.puColumns[1] = 6; + } } break; case LVN_DELETEITEM: @@ -2383,6 +2390,205 @@ static void test_customdraw_background(BOOL v6) } } +static void test_tileview_columns_info(BOOL v6) +{ + /* There are two ways to set and get tile info for columns, with LVM_GETITEMA + * messages and the LVIF_COLUMNS; and with LVM_GETTILEINFO messages. + * Both support passing NULL as the puColumns to request that the correct cColumns + * field is set; but only LVM_GETITEMA provides cromulent errors when the buffer + * is undersized. */ + HWND hwnd; + LVITEMA item; + DWORD r; + static UINT puColumns[2] = {1,2}; + static UINT puColumnsNext[3] = {3,2,1}; + + /* Providing a buffer large enough to hold all the values */ + static UINT OVERSIZED = 4; + UINT puColumnsReturn[4] = {9,9,9,9}; + LVTILEINFO tvi = {0}; + + hwnd = create_listview_control(LVS_REPORT); + ok(hwnd != NULL, "failed to create a listview window\n"); + + memset(&item, 0, sizeof(item)); + item.mask = LVIF_COLUMNS; + item.iItem = 0; + item.cColumns = 2; + item.puColumns = puColumns; + r = SendMessageA(hwnd, LVM_INSERTITEMA, 0, (LPARAM)&item); + expect(0, r); + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + /* Ensure the List View has its own copy by changing puColumns */ + puColumns[0] = 8; + + memset(&item, 0, sizeof(item)); + item.iItem = 0; + item.mask = LVIF_COLUMNS; + item.cColumns = OVERSIZED; + item.puColumns = puColumnsReturn; + r = SendMessageA(hwnd, LVM_GETITEMA, 0, (LPARAM)&item); + ok(r, "LVM_GETITEMA should return OK for correct Column get.\n"); + expect(2, item.cColumns); + + ok(item.puColumns == puColumnsReturn, "Should write to passed pointer, not change the pointer.\n"); + if (item.puColumns != NULL) { + expect(1, item.puColumns[0]); + expect(2, item.puColumns[1]); + } + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + memset(&item, 0, sizeof(item)); + item.iItem = 0; + item.mask = LVIF_COLUMNS; + item.cColumns = 1; + item.puColumns = puColumnsReturn; + r = SendMessageA(hwnd, LVM_GETITEMA, 0, (LPARAM)&item); + ok(!r, "LVM_GETITEMA returns Error if buffer is undersized.\n"); + expect(1, item.cColumns); + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + memset(&item, 0, sizeof(item)); + item.iItem = 0; + item.mask = LVIF_COLUMNS; + item.cColumns = 0; + item.puColumns = NULL; + r = SendMessageA(hwnd, LVM_GETITEMA, 0, (LPARAM)&item); + ok(r, "LVM_GETITEMA return OK for Query Style (no buffer supplied).\n"); + expect(2, item.cColumns); + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + memset(&tvi, 0, sizeof(tvi)); + + tvi.cbSize = sizeof(tvi); + tvi.iItem = 0; + tvi.cColumns = 3; + tvi.puColumns = puColumnsNext; + + r = SendMessageA(hwnd, LVM_SETTILEINFO, 0, (LPARAM)&tvi); + ok(r, "Failed to set item tile info.\n"); + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + /* Again, ensure Ownership */ + puColumnsNext[0] = 8; + + /* Good LVM_GETTILEINFO get with buffer and large enough size */ + tvi.cColumns = OVERSIZED; + tvi.puColumns = puColumnsReturn; + r = SendMessageA(hwnd, LVM_GETTILEINFO, 0, (LPARAM)&tvi); + ok(r, "LVM_GETTILEINFO Should retrieve info when parameters are ok.\n"); + expect(3, tvi.cColumns); + + ok(tvi.puColumns == puColumnsReturn, "Should write to passed pointer, not change the pointer.\n"); + if (tvi.puColumns != NULL) { + expect(3, tvi.puColumns[0]); + expect(2, tvi.puColumns[1]); + expect(1, tvi.puColumns[2]); + // Shouldn't touch, beyound reach. + expect(9, tvi.puColumns[3]); + } + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + /* Reset return buffer */ + puColumnsReturn[0] = 9; + puColumnsReturn[1] = 9; + puColumnsReturn[2] = 9; + + /* Undersized get with buffer */ + tvi.cColumns = 1; + tvi.puColumns = puColumnsReturn; + r = SendMessageA(hwnd, LVM_GETTILEINFO, 0, (LPARAM)&tvi); + ok(r, "LVM_GETTILEINFO returns OK with bad results\n"); + expect(1, tvi.cColumns); + ok(tvi.puColumns == puColumnsReturn, "Should write to passed pointer, not change the pointer.\n"); + if (tvi.puColumns != NULL) { + // Shouldn't touch + expect(9, tvi.puColumns[0]); + expect(9, tvi.puColumns[1]); + expect(9, tvi.puColumns[2]); + expect(9, tvi.puColumns[3]); + } + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + /* Undersized get without buffer */ + tvi.cColumns = 1; + tvi.puColumns = NULL; + r = SendMessageA(hwnd, LVM_GETTILEINFO, 0, (LPARAM)&tvi); + ok(r, "LVM_GETTILEINFO returns OK with size Query\n"); + expect(3, tvi.cColumns); + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + /* Set with Callback Question */ + tvi.cbSize = sizeof(tvi); + tvi.iItem = 0; + tvi.cColumns = I_COLUMNSCALLBACK; + tvi.puColumns = NULL; + + r = SendMessageA(hwnd, LVM_SETTILEINFO, 0, (LPARAM)&tvi); + ok(r, "Failed to set item tile info.\n"); + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + /* Good LVM_GETTILEINFO get callback */ + tvi.cColumns = OVERSIZED; + tvi.puColumns = NULL; + r = SendMessageA(hwnd, LVM_GETTILEINFO, 0, (LPARAM)&tvi); + ok(r, "LVM_GETTILEINFO Should retrieve info from callback function.\n"); + expect(LVIF_COLUMNS, g_itema.mask); + expect(OVERSIZED, g_itema.cColumns); + ok(g_itema.puColumns != NULL, "Callback should receive write location.\n"); + expect(2, tvi.cColumns); + + ok_sequence(sequences, PARENT_SEQ_INDEX, single_getdispinfo_parent_seq, + "get cCallback dispinfo", FALSE); + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + + DestroyWindow(hwnd); +} + +static void test_get_set_tileview_info(void) +{ + HWND hwnd; + DWORD r; + + LVTILEVIEWINFO tvi = {0}; + SIZE size = { 100, 50 }; + + hwnd = create_listview_control(LVS_REPORT); + ok(hwnd != NULL, "failed to create a listview window\n"); + + memset(&tvi, 0, sizeof(tvi)); + + tvi.cbSize = sizeof(tvi); + tvi.cLines = 3; + tvi.sizeTile = size; + tvi.dwFlags = LVTVIF_FIXEDSIZE; + tvi.dwMask = LVTVIM_COLUMNS | LVTVIM_TILESIZE; + r = SendMessageA(hwnd, LVM_SETTILEVIEWINFO, 0, (LPARAM)&tvi); + ok(r, "Failed to set item tile info.\n"); + + tvi.cLines = 0; + tvi.sizeTile.cx = 0; + tvi.sizeTile.cy = 0; + + r = SendMessageA(hwnd, LVM_GETTILEVIEWINFO, 0, (LPARAM)&tvi); + ok(r, "Failed to set item tile info.\n"); + expect(3, tvi.cLines); + expect(100, tvi.sizeTile.cx); + expect(50, tvi.sizeTile.cy); + + flush_sequences(sequences, NUM_MSG_SEQUENCES); + DestroyWindow(hwnd); +} + + static void test_icon_spacing(void) { /* LVM_SETICONSPACING */ @@ -7916,6 +8122,8 @@ START_TEST(listview) test_LVM_GETHOTCURSOR(); test_LVM_GETORIGIN(TRUE); test_customdraw_background(TRUE); + test_tileview_columns_info(TRUE); + test_get_set_tileview_info(); test_WM_PAINT(); uninit_winevent_hook(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10191
Hi, this looks interesting. However, this is one big patch containing many unrelated whitespace changes. Please try to split the patch into smaller patches for easier review. Try adding tests first. LV_VIEW_TILE seems to be available only for v6. So you probably need to add some "#if __WINE_COMCTL32_VERSION == 6" checks. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10191#note_131090
Yeah no worries. I did struggle a little with minimising whitespaces changes on this file already unfortunately though. There are a lot of bad practices with serveral thousand tab characters and hundreds of lines with trailing whitespace. Should the VERSION 6 guard just cover entering the tile view (like in `LISTVIEW_SetView`) or should it be more encompassing? Unfortunately, layout concerns currently touch a lot of areas in this component. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10191#note_131099
On Tue Mar 3 11:18:04 2026 +0000, Huw Campbell wrote:
Yeah no worries. I did struggle a little with minimising whitespaces changes on this file already unfortunately though. There are a lot of bad practices with serveral thousand tab characters and hundreds of lines with trailing whitespace. Should the VERSION 6 guard just cover entering the tile view (like in `LISTVIEW_SetView`) or should it be more encompassing? Unfortunately, layout concerns currently touch a lot of areas in this component. I'd say SetView() and get/set points where related item data is handled.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10191#note_131101
On Wed Mar 4 04:44:00 2026 +0000, Zhiyi Zhang wrote:
Hi, this looks interesting. However, this is one big patch containing many unrelated whitespace changes. Please try to split the patch into smaller patches for easier review. Try adding tests first. LV_VIEW_TILE seems to be available only for v6. So you probably need to add some "#if __WINE_COMCTL32_VERSION == 6" checks. So just to clarify? Would you like me to split this into two PRs, one with just tests, one with the implementation?
Or is having one Ok if I trim it down a bit. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10191#note_131189
On Wed Mar 4 04:44:00 2026 +0000, Huw Campbell wrote:
So just to clarify? Would you like me to split this into two PRs, one with just tests, one with the implementation? Or is having one Ok if I trim it down a bit. One PR with smaller commits. Please see https://gitlab.winehq.org/wine/wine/-/wikis/Submitting-Patches#patch-guideli...
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10191#note_131190
participants (4)
-
Huw Campbell -
Huw Campbell (@HuwCampbell) -
Nikolay Sivov (@nsivov) -
Zhiyi Zhang (@zhiyi)