2018-06-09 23:49:44 -05:00
|
|
|
// 10 june 2018
|
|
|
|
#include "uipriv_windows.hpp"
|
|
|
|
#include "table.hpp"
|
|
|
|
|
|
|
|
/*
|
|
|
|
This file handles both images and checkboxes in tables.
|
|
|
|
|
|
|
|
For images, we'll do a similar thing to what text columns do: cycle out images from the small image list every few LVN_GETDISPINFO notifications.
|
|
|
|
|
|
|
|
For checkboxes, the native list view checkbox functionality uses state images, but those are only supported on the main item, not on subitems. So instead, we'll do them on normal images instead.
|
|
|
|
TODO will this affect accessibility?
|
|
|
|
|
|
|
|
We'll use the small image list. For this, the first few items will be reserved for checkboxes, and the last few for cell images.
|
|
|
|
*/
|
|
|
|
|
2018-06-10 09:43:29 -05:00
|
|
|
// checkboxes TODOs:
|
|
|
|
// - see if we need to get rid of the extra margin in subitems
|
|
|
|
// - see if we need to get rid of the glow effect
|
|
|
|
|
2018-06-09 23:49:44 -05:00
|
|
|
#define nCheckboxImages 4
|
|
|
|
|
|
|
|
static HRESULT setCellImage(uiTable *t, NMLVDISPINFOW *nm, uiprivTableColumnParams *p, uiTableData *data)
|
|
|
|
{
|
|
|
|
int index;
|
|
|
|
HDC dc;
|
2018-06-10 09:43:29 -05:00
|
|
|
IWICBitmap *wb;
|
2018-06-09 23:49:44 -05:00
|
|
|
HBITMAP b;
|
|
|
|
|
|
|
|
index = t->smallIndex + nCheckboxImages;
|
|
|
|
t->smallIndex++;
|
|
|
|
t->smallIndex %= uiprivNumLVN_GETDISPINFOSkip;
|
|
|
|
nm->item.iImage = index;
|
|
|
|
|
|
|
|
dc = GetDC(t->hwnd);
|
|
|
|
if (dc == NULL) {
|
|
|
|
logLastError(L"GetDC()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
wb = uiprivImageAppropriateForDC(uiTableDataImage(data), dc);
|
|
|
|
b = uiprivWICToGDI(wb, dc);
|
2018-06-10 09:43:29 -05:00
|
|
|
// TODO rewrite this condition to make more sense; possibly swap the if and else blocks too
|
|
|
|
if (ImageList_GetImageCount(t->smallImages) > index) {
|
|
|
|
if (ImageList_Replace(t->smallImages, index, b, NULL) == 0) {
|
|
|
|
logLastError(L"ImageList_Replace()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
} else
|
|
|
|
if (ImageList_Add(t->smallImages, b, NULL) == -1) {
|
|
|
|
logLastError(L"ImageList_Add()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
2018-06-09 23:49:44 -05:00
|
|
|
|
|
|
|
if (ReleaseDC(t->hwnd, dc) == 0) {
|
|
|
|
logLastError(L"ReleaseDC()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
return S_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
HRESULT uiprivLVN_GETDISPINFOImagesCheckboxes(uiTable *t, NMLVDISPINFOW *nm, uiprivTableColumnParams *p)
|
|
|
|
{
|
|
|
|
uiTableData *data;
|
|
|
|
HRESULT hr;
|
|
|
|
|
|
|
|
if ((nm->item.mask & LVIF_IMAGE) == 0)
|
2018-06-10 09:43:29 -05:00
|
|
|
// TODO we actually need to do the first column fix here too...
|
2018-06-09 23:49:44 -05:00
|
|
|
return S_OK; // nothing to do here
|
|
|
|
|
|
|
|
if (p->imageModelColumn != -1) {
|
|
|
|
data = (*(t->model->mh->CellValue))(t->model->mh, t->model, nm->item.iItem, p->imageModelColumn);
|
|
|
|
hr = setCellImage(t, nm, p, data);
|
|
|
|
uiFreeTableData(data);
|
|
|
|
return hr;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (p->checkboxModelColumn != -1) {
|
2018-06-10 09:43:29 -05:00
|
|
|
#if 0
|
2018-06-09 23:49:44 -05:00
|
|
|
// TODO handle enabled
|
|
|
|
data = (*(t->model->mh->CellValue))(t->model->mh, t->model, nm->item.iItem, p->textModelColumn);
|
|
|
|
checked = uiTableDataInt(data) != 0;
|
|
|
|
uiFreeTableData(data);
|
|
|
|
nm->item.iImage = 0;
|
|
|
|
if (checked)
|
|
|
|
nm->item.iImage = 1;
|
|
|
|
nm->item.mask |= LVIF_IMAGE;
|
|
|
|
#endif
|
2018-06-10 09:43:29 -05:00
|
|
|
nm->item.mask |= LVIF_IMAGE;
|
2018-06-10 09:45:50 -05:00
|
|
|
nm->item.iImage = nm->item.iItem % 4;
|
2018-06-10 09:43:29 -05:00
|
|
|
return S_OK;
|
|
|
|
}
|
2018-06-09 23:49:44 -05:00
|
|
|
|
|
|
|
// if we got here, there's no image in this cell
|
2018-06-10 09:43:29 -05:00
|
|
|
nm->item.mask &= ~LVIF_IMAGE;
|
2018-06-09 23:49:44 -05:00
|
|
|
// having an image list always leaves space for an image on the main item :|
|
|
|
|
// other places on the internet imply that you should be able to do this but that it shouldn't work
|
|
|
|
// but it works perfectly (and pixel-perfectly too) for me, so...
|
2018-06-10 09:43:29 -05:00
|
|
|
// TODO it doesn't work anymore...
|
2018-06-09 23:49:44 -05:00
|
|
|
if (nm->item.iSubItem == 0) {
|
|
|
|
nm->item.mask |= LVIF_INDENT;
|
|
|
|
nm->item.iIndent = -1;
|
|
|
|
}
|
|
|
|
return S_OK;
|
|
|
|
}
|
2018-06-10 09:43:29 -05:00
|
|
|
|
2018-06-10 12:15:21 -05:00
|
|
|
// references for checkbox drawing:
|
|
|
|
// - https://blogs.msdn.microsoft.com/oldnewthing/20171129-00/?p=97485
|
|
|
|
// - https://blogs.msdn.microsoft.com/oldnewthing/20171201-00/?p=97505
|
|
|
|
|
2018-06-10 09:43:29 -05:00
|
|
|
static UINT unthemedStates[] = {
|
|
|
|
0,
|
|
|
|
DFCS_CHECKED,
|
|
|
|
DFCS_INACTIVE,
|
|
|
|
DFCS_CHECKED | DFCS_INACTIVE,
|
|
|
|
};
|
|
|
|
|
2018-06-10 12:15:21 -05:00
|
|
|
static int themedStates[] = {
|
|
|
|
CBS_UNCHECKEDNORMAL,
|
|
|
|
CBS_CHECKEDNORMAL,
|
|
|
|
CBS_UNCHECKEDDISABLED,
|
|
|
|
CBS_CHECKEDDISABLED,
|
|
|
|
};
|
|
|
|
|
2018-06-10 09:43:29 -05:00
|
|
|
// TODO properly clean up on failure
|
2018-06-10 12:15:21 -05:00
|
|
|
static HRESULT mkCheckboxes(uiTable *t, HTHEME theme, HDC dc, int cxList, int cyList, int cxCheck, int cyCheck)
|
2018-06-10 09:43:29 -05:00
|
|
|
{
|
|
|
|
BITMAPINFO bmi;
|
|
|
|
HBITMAP b;
|
|
|
|
VOID *bits;
|
|
|
|
HDC cdc;
|
|
|
|
HBITMAP prevBitmap;
|
|
|
|
RECT r;
|
|
|
|
int i;
|
2018-06-10 12:15:21 -05:00
|
|
|
HRESULT hr;
|
2018-06-10 09:43:29 -05:00
|
|
|
|
|
|
|
ZeroMemory(&bmi, sizeof (BITMAPINFO));
|
|
|
|
bmi.bmiHeader.biSize = sizeof (BITMAPINFOHEADER);
|
2018-06-10 12:15:21 -05:00
|
|
|
bmi.bmiHeader.biWidth = cxList * nCheckboxImages;
|
|
|
|
bmi.bmiHeader.biHeight = cyList;
|
2018-06-10 09:43:29 -05:00
|
|
|
bmi.bmiHeader.biPlanes = 1;
|
|
|
|
bmi.bmiHeader.biBitCount = 32;
|
|
|
|
bmi.bmiHeader.biCompression = BI_RGB;
|
|
|
|
b = CreateDIBSection(dc, &bmi, DIB_RGB_COLORS,
|
|
|
|
&bits, NULL, 0);
|
|
|
|
if (b == NULL) {
|
|
|
|
logLastError(L"CreateDIBSection()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
cdc = CreateCompatibleDC(dc);
|
|
|
|
if (cdc == NULL) {
|
|
|
|
logLastError(L"CreateCompatibleDC()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
prevBitmap = (HBITMAP) SelectObject(cdc, b);
|
|
|
|
if (prevBitmap == NULL) {
|
|
|
|
logLastError(L"SelectObject() b");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// the actual list view LVS_EX_CHECKBOXES code does this to ensure the entire image is valid, not just the parts that are drawn after resizing
|
|
|
|
// TODO find a better, alpha-friendly way to do this
|
2018-06-10 12:15:21 -05:00
|
|
|
// note that the actual list view does this only if unthemed, but it can get away with that since its image lists only contain checkmarks
|
|
|
|
// ours don't, so we have to compromise until the above TODO is resolved so we don't draw alpha stuff on top of garbage
|
|
|
|
if (theme == NULL || cxList != cxCheck || cyList != cyCheck) {
|
|
|
|
r.left = 0;
|
|
|
|
r.top = 0;
|
|
|
|
r.right = cxList * nCheckboxImages;
|
|
|
|
r.bottom = cyList;
|
|
|
|
FillRect(cdc, &r, GetSysColorBrush(COLOR_WINDOW));
|
|
|
|
}
|
2018-06-10 09:43:29 -05:00
|
|
|
|
|
|
|
r.left = 0;
|
|
|
|
r.top = 0;
|
2018-06-10 12:15:21 -05:00
|
|
|
r.right = cxCheck;
|
|
|
|
r.bottom = cyCheck;
|
|
|
|
if (theme != NULL) {
|
|
|
|
// because we're not making an image list exactly the correct size, we'll need to manually position the checkbox correctly
|
|
|
|
// let's just center it for now
|
|
|
|
// TODO make sure this is correct...
|
|
|
|
r.left = (cxList - cxCheck) / 2;
|
|
|
|
r.top = (cyList - cyCheck) / 2;
|
|
|
|
r.right += r.left;
|
|
|
|
r.bottom += r.top;
|
|
|
|
for (i = 0; i < nCheckboxImages; i++) {
|
|
|
|
hr = DrawThemeBackground(theme, cdc,
|
|
|
|
BP_CHECKBOX, themedStates[i],
|
|
|
|
&r, NULL);
|
|
|
|
if (hr != S_OK) {
|
|
|
|
logHRESULT(L"DrawThemeBackground()", hr);
|
|
|
|
return hr;
|
|
|
|
}
|
|
|
|
r.left += cxList;
|
|
|
|
r.right += cxList;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// this is what the actual list view LVS_EX_CHECKBOXES code does to more correctly size the checkboxes
|
|
|
|
// TODO check errors
|
|
|
|
InflateRect(&r, -GetSystemMetrics(SM_CXEDGE), -GetSystemMetrics(SM_CYEDGE));
|
|
|
|
r.right++;
|
|
|
|
r.bottom++;
|
|
|
|
for (i = 0; i < nCheckboxImages; i++) {
|
|
|
|
if (DrawFrameControl(cdc, &r,
|
|
|
|
DFC_BUTTON, DFCS_BUTTONCHECK | DFCS_FLAT | unthemedStates[i]) == 0) {
|
|
|
|
logLastError(L"DrawFrameControl()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
r.left += cxList;
|
|
|
|
r.right += cxList;
|
2018-06-10 09:43:29 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SelectObject(cdc, prevBitmap) != ((HGDIOBJ) b)) {
|
|
|
|
logLastError(L"SelectObject() prev");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
if (DeleteDC(cdc) == 0) {
|
|
|
|
logLastError(L"DeleteDC()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ImageList_Add(t->smallImages, b, NULL) == -1) {
|
|
|
|
logLastError(L"ImageList_Add()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO error check
|
|
|
|
DeleteObject(b);
|
|
|
|
return S_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO run again when the DPI changes
|
|
|
|
HRESULT uiprivTableSetupImagesCheckboxes(uiTable *t)
|
|
|
|
{
|
2018-06-10 12:15:21 -05:00
|
|
|
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;
|
|
|
|
}
|
2018-06-10 09:43:29 -05:00
|
|
|
|
|
|
|
// TODO handle errors
|
2018-06-10 12:15:21 -05:00
|
|
|
t->smallImages = ImageList_Create(cxList, cyList,
|
2018-06-10 09:43:29 -05:00
|
|
|
ILC_COLOR32,
|
|
|
|
nCheckboxImages + uiprivNumLVN_GETDISPINFOSkip, nCheckboxImages + uiprivNumLVN_GETDISPINFOSkip);
|
|
|
|
if (t->smallImages == 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->smallImages));
|
2018-06-10 12:15:21 -05:00
|
|
|
hr = mkCheckboxes(t, theme, dc, cxList, cyList, sizeCheck.cx, sizeCheck.cy);
|
|
|
|
if (hr != S_OK)
|
|
|
|
return hr;
|
|
|
|
|
|
|
|
hr = CloseThemeData(theme);
|
|
|
|
if (hr != S_OK) {
|
|
|
|
logHRESULT(L"CloseThemeData()", hr);
|
|
|
|
return hr;
|
|
|
|
}
|
|
|
|
if (ReleaseDC(t->hwnd, dc) == 0) {
|
|
|
|
logLastError(L"ReleaseDC()");
|
|
|
|
return E_FAIL;
|
|
|
|
}
|
|
|
|
return S_OK;
|
2018-06-10 09:43:29 -05:00
|
|
|
}
|