// 14 june 2018 #include "uipriv_windows.hpp" #include "table.hpp" // TODOs: // - properly hide selection when not focused (or switch on LVS_SHOWSELALWAYS and draw that state) // TODO #define cellValue(model, row, column) ((*((model)->mh->CellValue))((model)->mh, (model), (row), (column))) // TODO maybe split this into item and subitem structs? struct drawState { uiTable *t; uiTableModel *model; uiprivTableColumnParams *p; HDC dc; int iItem; int iSubItem; uiprivTableMetrics *m; COLORREF bgColor; HBRUSH bgBrush; BOOL freeBgBrush; COLORREF textColor; HBRUSH textBrush; BOOL freeTextBrush; }; static HRESULT drawBackgrounds(HRESULT hr, struct drawState *s) { if (hr != S_OK) return hr; if (s->m->hasImage) if (FillRect(s->dc, &(s->m->subitemIcon), GetSysColorBrush(COLOR_WINDOW)) == 0) { logLastError(L"FillRect() icon"); return E_FAIL; } if (FillRect(s->dc, &(s->m->realTextBackground), s->bgBrush) == 0) { logLastError(L"FillRect()"); return E_FAIL; } return S_OK; } static void centerImageRect(RECT *image, RECT *space) { LONG xoff, yoff; // first make sure both have the same upper-left xoff = image->left - space->left; yoff = image->top - space->top; image->left -= xoff; image->top -= yoff; image->right -= xoff; image->bottom -= yoff; // now center xoff = ((space->right - space->left) - (image->right - image->left)) / 2; yoff = ((space->bottom - space->top) - (image->bottom - image->top)) / 2; image->left += xoff; image->top += yoff; image->right += xoff; image->bottom += yoff; } static HRESULT drawImagePart(HRESULT hr, struct drawState *s) { uiTableData *data; IWICBitmap *wb; HBITMAP b; RECT r; UINT fStyle; if (hr != S_OK) return hr; if (s->p->imageModelColumn == -1) return S_OK; data = cellValue(s->model, s->iItem, s->p->imageModelColumn); wb = uiprivImageAppropriateForDC(uiTableDataImage(data), s->dc); uiFreeTableData(data); hr = uiprivWICToGDI(wb, s->dc, s->m->cxIcon, s->m->cyIcon, &b); if (hr != S_OK) return hr; // TODO rewrite this condition to make more sense; possibly swap the if and else blocks too // TODO proper cleanup if (ImageList_GetImageCount(s->t->imagelist) > 1) { if (ImageList_Replace(s->t->imagelist, 0, b, NULL) == 0) { logLastError(L"ImageList_Replace()"); return E_FAIL; } } else if (ImageList_Add(s->t->imagelist, b, NULL) == -1) { logLastError(L"ImageList_Add()"); return E_FAIL; } // TODO error check DeleteObject(b); r = s->m->subitemIcon; r.right = r.left + s->m->cxIcon; r.bottom = r.top + s->m->cyIcon; centerImageRect(&r, &(s->m->subitemIcon)); fStyle = ILD_NORMAL; if (s->m->selected) fStyle = ILD_SELECTED; if (ImageList_Draw(s->t->imagelist, 0, s->dc, r.left, r.top, fStyle) == 0) { logLastError(L"ImageList_Draw()"); return E_FAIL; } return S_OK; } // references for checkbox drawing: // - https://blogs.msdn.microsoft.com/oldnewthing/20171129-00/?p=97485 // - https://blogs.msdn.microsoft.com/oldnewthing/20171201-00/?p=97505 static HRESULT drawUnthemedCheckbox(struct drawState *s, int checked, int enabled) { RECT r; UINT state; r = s->m->subitemIcon; // this is what the actual list view LVS_EX_CHECKBOXES code does to size the checkboxes // TODO reverify the initial size r.right = r.left + GetSystemMetrics(SM_CXSMICON); r.bottom = r.top + GetSystemMetrics(SM_CYSMICON); if (InflateRect(&r, -GetSystemMetrics(SM_CXEDGE), -GetSystemMetrics(SM_CYEDGE)) == 0) { logLastError(L"InflateRect()"); return E_FAIL; } r.right++; r.bottom++; centerImageRect(&r, &(s->m->subitemIcon)); state = DFCS_BUTTONCHECK | DFCS_FLAT; if (checked) state |= DFCS_CHECKED; if (!enabled) state |= DFCS_INACTIVE; if (DrawFrameControl(s->dc, &r, DFC_BUTTON, state) == 0) { logLastError(L"DrawFrameControl()"); return E_FAIL; } return S_OK; } static HRESULT drawThemedCheckbox(struct drawState *s, HTHEME theme, int checked, int enabled) { RECT r; SIZE size; int state; HRESULT hr; hr = GetThemePartSize(theme, s->dc, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, NULL, TS_DRAW, &size); if (hr != S_OK) { logHRESULT(L"GetThemePartSize()", hr); return hr; // TODO fall back? } r = s->m->subitemIcon; r.right = r.left + size.cx; r.bottom = r.top + size.cy; centerImageRect(&r, &(s->m->subitemIcon)); if (!checked && enabled) state = CBS_UNCHECKEDNORMAL; else if (checked && enabled) state = CBS_CHECKEDNORMAL; else if (!checked && !enabled) state = CBS_UNCHECKEDDISABLED; else state = CBS_CHECKEDDISABLED; hr = DrawThemeBackground(theme, s->dc, BP_CHECKBOX, state, &r, NULL); if (hr != S_OK) { logHRESULT(L"DrawThemeBackground()", hr); return hr; } return S_OK; } static HRESULT drawCheckboxPart(HRESULT hr, struct drawState *s) { uiTableData *data; int checked, enabled; HTHEME theme; if (hr != S_OK) return hr; if (s->p->checkboxModelColumn == -1) return S_OK; data = cellValue(s->model, s->iItem, s->p->checkboxModelColumn); checked = uiTableDataInt(data); uiFreeTableData(data); switch (s->p->checkboxEditableColumn) { case uiTableModelColumnNeverEditable: enabled = 0; break; case uiTableModelColumnAlwaysEditable: enabled = 1; break; default: data = cellValue(s->model, s->iItem, s->p->checkboxEditableColumn); enabled = uiTableDataInt(data); uiFreeTableData(data); } theme = OpenThemeData(s->t->hwnd, L"button"); if (theme != NULL) { hr = drawThemedCheckbox(s, theme, checked, enabled); if (hr != S_OK) return hr; hr = CloseThemeData(theme); if (hr != S_OK) { logHRESULT(L"CloseThemeData()", hr); return hr; } } else { hr = drawUnthemedCheckbox(s, checked, enabled); if (hr != S_OK) return hr; } return S_OK; } static HRESULT drawTextPart(HRESULT hr, struct drawState *s) { COLORREF prevText; int prevMode; RECT r; uiTableData *data; WCHAR *wstr; if (hr != S_OK) return hr; if (!s->m->hasText) return S_OK; // don't draw the text underneath an edit control if (s->t->edit != NULL && s->t->editedItem == s->iItem && s->t->editedSubitem == s->iSubItem) return S_OK; prevText = SetTextColor(s->dc, s->textColor); if (prevText == CLR_INVALID) { logLastError(L"SetTextColor()"); return E_FAIL; } prevMode = SetBkMode(s->dc, TRANSPARENT); if (prevMode == 0) { logLastError(L"SetBkMode()"); return E_FAIL; } data = cellValue(s->model, s->iItem, s->p->textModelColumn); wstr = toUTF16(uiTableDataString(data)); uiFreeTableData(data); // These flags are a menagerie of flags from various sources: // guessing, the Windows 2000 source leak, various custom // draw examples on the web, etc. // TODO find the real correct flags if (DrawTextW(s->dc, wstr, -1, &(s->m->realTextRect), DT_LEFT | DT_VCENTER | DT_END_ELLIPSIS | DT_SINGLELINE | DT_NOPREFIX | DT_EDITCONTROL) == 0) { uiprivFree(wstr); logLastError(L"DrawTextW()"); return E_FAIL; } uiprivFree(wstr); // TODO decide once and for all what to compare to here and with SelectObject() if (SetBkMode(s->dc, prevMode) != TRANSPARENT) { logLastError(L"SetBkMode() prev"); return E_FAIL; } if (SetTextColor(s->dc, prevText) != s->textColor) { logLastError(L"SetTextColor() prev"); return E_FAIL; } return S_OK; } // much of this is to imitate what shell32.dll's CDrawProgressBar does #define indeterminateSegments 8 static HRESULT drawProgressBarPart(HRESULT hr, struct drawState *s) { int progress; LONG indeterminatePos; HTHEME theme; RECT r; RECT rBorder, rFill[2]; int i, nFill; TEXTMETRICW tm; int sysColor; if (hr != S_OK) return hr; if (s->p->progressBarModelColumn == -1) return S_OK; progress = uiprivTableProgress(s->t, s->iItem, s->iSubItem, s->p->progressBarModelColumn, &indeterminatePos); theme = OpenThemeData(s->t->hwnd, L"PROGRESS"); if (GetTextMetricsW(s->dc, &tm) == 0) { logLastError(L"GetTextMetricsW()"); hr = E_FAIL; goto fail; } r = s->m->subitemBounds; // this sets the height of the progressbar and vertically centers it in one fell swoop r.top += (r.bottom - tm.tmHeight - r.top) / 2; r.bottom = r.top + tm.tmHeight; // TODO check errors rBorder = r; InflateRect(&rBorder, -1, -1); if (theme != NULL) { RECT crect; hr = GetThemeBackgroundContentRect(theme, s->dc, PP_TRANSPARENTBAR, PBBS_NORMAL, &rBorder, &crect); if (hr != S_OK) { logHRESULT(L"GetThemeBackgroundContentRect()", hr); goto fail; } hr = DrawThemeBackground(theme, s->dc, PP_TRANSPARENTBAR, PBBS_NORMAL, &crect, NULL); if (hr != S_OK) { logHRESULT(L"DrawThemeBackground() border", hr); goto fail; } } else { HPEN pen, prevPen; HBRUSH brush, prevBrush; sysColor = COLOR_HIGHLIGHT; if (s->m->selected) sysColor = COLOR_HIGHLIGHTTEXT; // TODO check errors everywhere pen = CreatePen(PS_SOLID, 1, GetSysColor(sysColor)); prevPen = (HPEN) SelectObject(s->dc, pen); brush = (HBRUSH) GetStockObject(NULL_BRUSH); prevBrush = (HBRUSH) SelectObject(s->dc, brush); Rectangle(s->dc, rBorder.left, rBorder.top, rBorder.right, rBorder.bottom); SelectObject(s->dc, prevBrush); SelectObject(s->dc, prevPen); DeleteObject(pen); } nFill = 1; rFill[0] = r; // TODO check error InflateRect(&rFill[0], -1, -1); if (progress != -1) rFill[0].right -= (rFill[0].right - rFill[0].left) * (100 - progress) / 100; else { LONG barWidth; LONG pieceWidth; // TODO explain all this // TODO this should really start the progressbar scrolling into view instead of already on screen when first set rFill[1] = rFill[0]; // save in case we need it barWidth = rFill[0].right - rFill[0].left; pieceWidth = barWidth / indeterminateSegments; rFill[0].left += indeterminatePos % barWidth; if ((rFill[0].left + pieceWidth) >= rFill[0].right) { // make this piece wrap back around nFill++; rFill[1].right = rFill[1].left + (pieceWidth - (rFill[0].right - rFill[0].left)); } else rFill[0].right = rFill[0].left + pieceWidth; } for (i = 0; i < nFill; i++) if (theme != NULL) { hr = DrawThemeBackground(theme, s->dc, PP_FILL, PBFS_NORMAL, &rFill[i], NULL); if (hr != S_OK) { logHRESULT(L"DrawThemeBackground() fill", hr); goto fail; } } else // TODO check errors FillRect(s->dc, &rFill[i], GetSysColorBrush(sysColor)); hr = S_OK; fail: // TODO check errors if (theme != NULL) CloseThemeData(theme); return hr; } static HRESULT drawButtonPart(HRESULT hr, struct drawState *s) { uiTableData *data; WCHAR *wstr; bool enabled; HTHEME theme; RECT r; TEXTMETRICW tm; if (hr != S_OK) return hr; if (s->p->buttonModelColumn == -1) return S_OK; data = cellValue(s->model, s->iItem, s->p->buttonModelColumn); wstr = toUTF16(uiTableDataString(data)); uiFreeTableData(data); switch (s->p->buttonClickableModelColumn) { case uiTableModelColumnNeverEditable: enabled = 0; break; case uiTableModelColumnAlwaysEditable: enabled = 1; break; default: data = cellValue(s->model, s->iItem, s->p->checkboxEditableColumn); enabled = uiTableDataInt(data); uiFreeTableData(data); } theme = OpenThemeData(s->t->hwnd, L"button"); if (GetTextMetricsW(s->dc, &tm) == 0) { logLastError(L"GetTextMetricsW()"); hr = E_FAIL; goto fail; } r = s->m->subitemBounds; if (theme != NULL) { int state; state = PBS_NORMAL; if (!enabled) state = PBS_DISABLED; hr = DrawThemeBackground(theme, s->dc, BP_PUSHBUTTON, state, &r, NULL); if (hr != S_OK) { logHRESULT(L"DrawThemeBackground()", hr); goto fail; } // TODO DT_EDITCONTROL? // TODO DT_PATH_ELLIPSIS DT_WORD_ELLIPSIS instead of DT_END_ELLIPSIS? a middle-ellipsis option would be ideal here // TODO is there a theme property we can get instead of hardcoding these flags? if not, make these flags a macro hr = DrawThemeText(theme, s->dc, BP_PUSHBUTTON, state, wstr, -1, DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS | DT_SINGLELINE | DT_NOPREFIX, 0, &r); if (hr != S_OK) { logHRESULT(L"DrawThemeText()", hr); goto fail; } } else { UINT state; HBRUSH color, prevColor; int prevBkMode; // TODO check errors // TODO explain why we're not doing this in the themed case (it has to do with extra transparent pixels) InflateRect(&r, -1, -1); state = DFCS_BUTTONPUSH; if (!enabled) state |= DFCS_INACTIVE; if (DrawFrameControl(s->dc, &r, DFC_BUTTON, state) == 0) { logLastError(L"DrawFrameControl()"); hr = E_FAIL; goto fail; } color = GetSysColorBrush(COLOR_BTNTEXT); // TODO check errors for these two prevColor = (HBRUSH) SelectObject(s->dc, color); prevBkMode = SetBkMode(s->dc, TRANSPARENT); // TODO DT_EDITCONTROL? // TODO DT_PATH_ELLIPSIS DT_WORD_ELLIPSIS instead of DT_END_ELLIPSIS? a middle-ellipsis option would be ideal here if (DrawTextW(s->dc, wstr, -1, &r, DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS | DT_SINGLELINE | DT_NOPREFIX) == 0) { logLastError(L"DrawTextW()"); hr = E_FAIL; goto fail; } // TODO check errors for these two SetBkMode(s->dc, prevBkMode); SelectObject(s->dc, prevColor); } hr = S_OK; fail: // TODO check errors if (theme != NULL) CloseThemeData(theme); uiprivFree(wstr); return hr; } static HRESULT freeDrawState(struct drawState *s) { HRESULT hr, hrret; hrret = S_OK; if (s->m != NULL) { uiprivFree(s->m); s->m = NULL; } if (s->freeTextBrush) { if (DeleteObject(s->textBrush) == 0) { logLastError(L"DeleteObject()"); hrret = E_FAIL; // continue cleaning up anyway } s->freeTextBrush = FALSE; } if (s->freeBgBrush) { if (DeleteObject(s->bgBrush) == 0) { logLastError(L"DeleteObject()"); hrret = E_FAIL; // continue cleaning up anyway } s->freeBgBrush = FALSE; } return hrret; } static COLORREF blend(COLORREF base, double r, double g, double b, double a) { double br, bg, bb; br = ((double) GetRValue(base)) / 255.0; bg = ((double) GetGValue(base)) / 255.0; bb = ((double) GetBValue(base)) / 255.0; br = (r * a) + (br * (1.0 - a)); bg = (g * a) + (bg * (1.0 - a)); bb = (b * a) + (bb * (1.0 - a)); return RGB((BYTE) (br * 255), (BYTE) (bg * 255), (BYTE) (bb * 255)); } static HRESULT fillDrawState(struct drawState *s, uiTable *t, NMLVCUSTOMDRAW *nm, uiprivTableColumnParams *p) { LRESULT state; HWND header; HRESULT hr; ZeroMemory(s, sizeof (struct drawState)); s->t = t; s->model = t->model; s->p = p; s->dc = nm->nmcd.hdc; s->iItem = nm->nmcd.dwItemSpec; s->iSubItem = nm->iSubItem; hr = uiprivTableGetMetrics(t, s->iItem, s->iSubItem, &(s->m)); if (hr != S_OK) goto fail; if (s->m->selected) { s->bgColor = GetSysColor(COLOR_HIGHLIGHT); s->bgBrush = GetSysColorBrush(COLOR_HIGHLIGHT); s->textColor = GetSysColor(COLOR_HIGHLIGHTTEXT); s->textBrush = GetSysColorBrush(COLOR_HIGHLIGHTTEXT); } else { uiTableData *data; double r, g, b, a; s->bgColor = GetSysColor(COLOR_WINDOW); s->bgBrush = GetSysColorBrush(COLOR_WINDOW); if (t->backgroundColumn != -1) { data = cellValue(s->model, s->iItem, t->backgroundColumn); if (data != NULL) { uiTableDataColor(data, &r, &g, &b, &a); uiFreeTableData(data); s->bgColor = blend(s->bgColor, r, g, b, a); s->bgBrush = CreateSolidBrush(s->bgColor); if (s->bgBrush == NULL) { logLastError(L"CreateSolidBrush()"); hr = E_FAIL; goto fail; } s->freeBgBrush = TRUE; } } s->textColor = GetSysColor(COLOR_WINDOWTEXT); s->textBrush = GetSysColorBrush(COLOR_WINDOWTEXT); if (p->textParams.ColorModelColumn != -1) { data = cellValue(s->model, s->iItem, p->textParams.ColorModelColumn); if (data != NULL) { uiTableDataColor(data, &r, &g, &b, &a); uiFreeTableData(data); s->textColor = blend(s->bgColor, r, g, b, a); s->textBrush = CreateSolidBrush(s->textColor); if (s->textBrush == NULL) { logLastError(L"CreateSolidBrush()"); hr = E_FAIL; goto fail; } s->freeTextBrush = TRUE; } } } return S_OK; fail: // ignore the error; we need to return the one we got above freeDrawState(s); return hr; } static HRESULT updateAndDrawFocusRects(HRESULT hr, uiTable *t, HDC dc, int iItem, RECT *realTextBackground, RECT *focus, bool *first) { LRESULT state; if (hr != S_OK) return hr; if (GetFocus() != t->hwnd) return S_OK; // uItemState CDIS_FOCUS doesn't quite work right because of bugs in the Windows list view that causes spurious redraws without the flag while we hover over the focused item // TODO only call this once state = SendMessageW(t->hwnd, LVM_GETITEMSTATE, (WPARAM) iItem, (LRESULT) (LVIS_FOCUSED)); if ((state & LVIS_FOCUSED) == 0) return S_OK; if (realTextBackground != NULL) if (*first) { *focus = *realTextBackground; *first = false; return S_OK; } else if (focus->right == realTextBackground->left) { focus->right = realTextBackground->right; return S_OK; } if (DrawFocusRect(dc, focus) == 0) { logLastError(L"DrawFocusRect()"); return E_FAIL; } if (realTextBackground != NULL) *focus = *realTextBackground; return S_OK; } // normally we would only draw stuff in subitem stages // this broke when we tried drawing focus rects in postpaint; the drawing either kept getting wiped or overdrawn, mouse hovering had something to do with it, and none of the "solutions" to this or similar problems on the internet worked // so now we do everything in the item prepaint stage // TODO consider switching to full-on owner draw // TODO only compute the background brushes once? HRESULT uiprivTableHandleNM_CUSTOMDRAW(uiTable *t, NMLVCUSTOMDRAW *nm, LRESULT *lResult) { struct drawState s; uiprivTableColumnParams *p; NMLVCUSTOMDRAW b; size_t i, n; RECT focus; bool focusFirst; HRESULT hr; switch (nm->nmcd.dwDrawStage) { case CDDS_PREPAINT: *lResult = CDRF_NOTIFYITEMDRAW; return S_OK; case CDDS_ITEMPREPAINT: break; default: *lResult = CDRF_DODEFAULT; return S_OK; } n = t->columns->size(); b = *nm; focusFirst = true; for (i = 0; i < n; i++) { b.iSubItem = i; p = (*(t->columns))[i]; hr = fillDrawState(&s, t, &b, p); if (hr != S_OK) return hr; hr = drawBackgrounds(S_OK, &s); hr = drawImagePart(hr, &s); hr = drawCheckboxPart(hr, &s); hr = drawTextPart(hr, &s); hr = drawProgressBarPart(hr, &s); hr = drawButtonPart(hr, &s); hr = updateAndDrawFocusRects(hr, s.t, s.dc, nm->nmcd.dwItemSpec, &(s.m->realTextBackground), &focus, &focusFirst); if (hr != S_OK) goto fail; hr = freeDrawState(&s); if (hr != S_OK) // TODO really error out here? return hr; } // and draw the last focus rect hr = updateAndDrawFocusRects(hr, t, nm->nmcd.hdc, nm->nmcd.dwItemSpec, NULL, &focus, &focusFirst); if (hr != S_OK) return hr; *lResult = CDRF_SKIPDEFAULT; return S_OK; fail: // ignore error here // TODO this is awkward cleanup placement for something that only really exists in a for loop freeDrawState(&s); return hr; } // TODO run again when the DPI or the theme changes // TODO properly clean things up here // TODO properly destroy the old lists here too HRESULT uiprivUpdateImageListSize(uiTable *t) { HDC dc; int cxList, cyList; HTHEME theme; SIZE sizeCheck; HRESULT hr; dc = GetDC(t->hwnd); if (dc == NULL) { logLastError(L"GetDC()"); return E_FAIL; } cxList = GetSystemMetrics(SM_CXSMICON); cyList = GetSystemMetrics(SM_CYSMICON); sizeCheck.cx = cxList; sizeCheck.cy = cyList; theme = OpenThemeData(t->hwnd, L"button"); if (theme != NULL) { hr = GetThemePartSize(theme, dc, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, NULL, TS_DRAW, &sizeCheck); if (hr != S_OK) { logHRESULT(L"GetThemePartSize()", hr); return hr; // TODO fall back? } // make sure these checkmarks fit // unthemed checkmarks will by the code above be smaller than cxList/cyList here if (cxList < sizeCheck.cx) cxList = sizeCheck.cx; if (cyList < sizeCheck.cy) cyList = sizeCheck.cy; hr = CloseThemeData(theme); if (hr != S_OK) { logHRESULT(L"CloseThemeData()", hr); return hr; } } // TODO handle errors t->imagelist = ImageList_Create(cxList, cyList, ILC_COLOR32, 1, 1); if (t->imagelist == NULL) { logLastError(L"ImageList_Create()"); return E_FAIL; } // TODO will this return NULL here because it's an initial state? SendMessageW(t->hwnd, LVM_SETIMAGELIST, LVSIL_SMALL, (LPARAM) (t->imagelist)); if (ReleaseDC(t->hwnd, dc) == 0) { logLastError(L"ReleaseDC()"); return E_FAIL; } return S_OK; }